diff --git a/CouchPotato.py b/CouchPotato.py old mode 100644 new mode 100755 index 88ba3436..57f6461c --- a/CouchPotato.py +++ b/CouchPotato.py @@ -4,124 +4,138 @@ from os.path import dirname import logging import os import signal +import socket import subprocess import sys import traceback -  -  + + # Root path base_path = dirname(os.path.abspath(__file__)) -  + # Insert local directories into path sys.path.insert(0, os.path.join(base_path, 'libs')) -  + from couchpotato.environment import Env from couchpotato.core.helpers.variable import getDataDir -  + class Loader(object): -  -    do_restart = False -  -    def __init__(self): -  -        # Get options via arg -        from couchpotato.runner import getOptions -        self.options = getOptions(base_path, sys.argv[1:]) -  -        # Load settings -        settings = Env.get('settings') -        settings.setFile(self.options.config_file) -  -        # Create data dir if needed -        self.data_dir = os.path.expanduser(Env.setting('data_dir')) -        if self.data_dir == '': -            self.data_dir = getDataDir() -  -        if not os.path.isdir(self.data_dir): -            os.makedirs(self.data_dir) -  -        # Create logging dir -        self.log_dir = os.path.join(self.data_dir, 'logs'); -        if not os.path.isdir(self.log_dir): -            os.mkdir(self.log_dir) -  -        # Logging -        from couchpotato.core.logger import CPLog -        self.log = CPLog(__name__) -  -        formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S') -        hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10) -        hdlr.setLevel(logging.CRITICAL) -        hdlr.setFormatter(formatter) -        self.log.logger.addHandler(hdlr) -  -    def addSignals(self): -  -        signal.signal(signal.SIGINT, self.onExit) -        signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) -  -        from couchpotato.core.event import addEvent -        addEvent('app.after_shutdown', self.afterShutdown) -  -    def afterShutdown(self, restart): -        self.do_restart = restart -  -    def onExit(self, signal, frame): -        from couchpotato.core.event import fireEvent -        fireEvent('app.crappy_shutdown', single = True) -  -    def run(self): -  -        self.addSignals() -  -        from couchpotato.runner import runCouchPotato -        runCouchPotato(self.options, base_path, sys.argv[1:], data_dir = self.data_dir, log_dir = self.log_dir, Env = Env) -  -        if self.do_restart: -            self.restart() -  -    def restart(self): -        try: -            # remove old pidfile first -            try: -                if self.runAsDaemon(): -                    self.daemon.delpid() -            except: -                self.log.critical(traceback.format_exc()) -  -            args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:] -            subprocess.Popen(args) -        except: -            self.log.critical(traceback.format_exc()) -  -    def daemonize(self): -  -        if self.runAsDaemon(): -            try: -                from daemon import Daemon -                self.daemon = Daemon(self.options.pid_file) -                self.daemon.daemonize() -            except SystemExit: -                raise -            except: -                self.log.critical(traceback.format_exc()) -  -    def runAsDaemon(self): -        return self.options.daemon and  self.options.pid_file -  -  + + do_restart = False + + def __init__(self): + + # Get options via arg + from couchpotato.runner import getOptions + self.options = getOptions(base_path, sys.argv[1:]) + + # Load settings + settings = Env.get('settings') + settings.setFile(self.options.config_file) + + # Create data dir if needed + self.data_dir = os.path.expanduser(Env.setting('data_dir')) + if self.data_dir == '': + self.data_dir = getDataDir() + + if not os.path.isdir(self.data_dir): + os.makedirs(self.data_dir) + + # Create logging dir + self.log_dir = os.path.join(self.data_dir, 'logs'); + if not os.path.isdir(self.log_dir): + os.mkdir(self.log_dir) + + # Logging + from couchpotato.core.logger import CPLog + self.log = CPLog(__name__) + + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S') + hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10) + hdlr.setLevel(logging.CRITICAL) + hdlr.setFormatter(formatter) + self.log.logger.addHandler(hdlr) + + def addSignals(self): + + signal.signal(signal.SIGINT, self.onExit) + signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) + + from couchpotato.core.event import addEvent + addEvent('app.after_shutdown', self.afterShutdown) + + def afterShutdown(self, restart): + self.do_restart = restart + + def onExit(self, signal, frame): + from couchpotato.core.event import fireEvent + fireEvent('app.crappy_shutdown', single = True) + + def run(self): + + self.addSignals() + + from couchpotato.runner import runCouchPotato + runCouchPotato(self.options, base_path, sys.argv[1:], data_dir = self.data_dir, log_dir = self.log_dir, Env = Env) + + if self.do_restart: + self.restart() + + def restart(self): + try: + # remove old pidfile first + try: + if self.runAsDaemon(): + try: self.daemon.stop() + except: pass + self.daemon.delpid() + except: + self.log.critical(traceback.format_exc()) + + args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:] + subprocess.Popen(args) + except: + self.log.critical(traceback.format_exc()) + + def daemonize(self): + + if self.runAsDaemon(): + try: + from daemon import Daemon + self.daemon = Daemon(self.options.pid_file) + self.daemon.daemonize() + except SystemExit: + raise + except: + self.log.critical(traceback.format_exc()) + + def runAsDaemon(self): + return self.options.daemon and self.options.pid_file + + if __name__ == '__main__': -    try: -        l = Loader() -        l.daemonize() -        l.run() -    except KeyboardInterrupt: -        pass -    except SystemExit: -        raise -    except Exception as (nr, msg): -        if nr != 4: -            try: -                l.log.critical(traceback.format_exc()) -            except: -                print traceback.format_exc() + try: + l = Loader() + l.daemonize() + l.run() + except KeyboardInterrupt: + pass + except SystemExit: + raise + except socket.error as (nr, msg): + # log when socket receives SIGINT, but continue. + # previous code would have skipped over other types of IO errors too. + if nr != 4: + try: + l.log.critical(traceback.format_exc()) + except: + print traceback.format_exc() + raise + except: + try: + # if this fails we will have two tracebacks + # one for failing to log, and one for the exception that got us here. + l.log.critical(traceback.format_exc()) + except: + print traceback.format_exc() + raise \ No newline at end of file diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 7058b111..799a2984 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -1,3 +1,4 @@ +from couchpotato.api import api_docs, api_docs_missing from couchpotato.core.auth import requires_auth from couchpotato.core.event import fireEvent from couchpotato.core.logger import CPLog @@ -35,6 +36,21 @@ def addView(route, func, static = False): def index(): return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) +""" Api view """ +@web.route('docs/') +@requires_auth +def apiDocs(): + from couchpotato import app + routes = [] + for route, x in sorted(app.view_functions.iteritems()): + if route[0:4] == 'api.': + routes += [route[4:].replace('::', '.')] + + if api_docs.get(''): + del api_docs[''] + del api_docs_missing[''] + return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing)) + @app.errorhandler(404) def page_not_found(error): index_url = url_for('web.index') diff --git a/couchpotato/api.py b/couchpotato/api.py index d1aa6a42..71cf9b87 100644 --- a/couchpotato/api.py +++ b/couchpotato/api.py @@ -1,21 +1,22 @@ -from couchpotato.core.helpers.request import jsonified from flask.blueprints import Blueprint +from flask.helpers import url_for +from flask.templating import render_template +from werkzeug.utils import redirect api = Blueprint('api', __name__) +api_docs = {} +api_docs_missing = [] -def addApiView(route, func, static = False): - api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '-') if route else 'index', view_func = func) +def addApiView(route, func, static = False, docs = None): + api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func) + if docs: + api_docs[route[4:] if route[0:4] == 'api.' else route] = docs + else: + api_docs_missing.append(route) """ Api view """ def index(): - from couchpotato import app - - routes = [] - for route, x in sorted(app.view_functions.iteritems()): - if route[0:4] == 'api.': - routes += [route[4:]] - - return jsonified({'routes': routes}) + index_url = url_for('web.index') + return redirect(index_url + 'docs/') addApiView('', index) -addApiView('default', index) diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index 38631a2e..d90fa59b 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/couchpotato/core/_base/_core/__init__.py @@ -6,6 +6,7 @@ def start(): config = [{ 'name': 'core', + 'order': 1, 'groups': [ { 'tab': 'general', @@ -54,7 +55,7 @@ config = [{ 'name': 'api_key', 'default': uuid4().hex, 'readonly': 1, - 'description': "This is top-secret! Don't share this!", + 'description': 'Let 3rd party app do stuff. Docs', }, { 'name': 'debug', @@ -80,13 +81,13 @@ config = [{ }, { 'name': 'permission_folder', - 'default': 0755, + 'default': '0755', 'label': 'Folder CHMOD', - 'description': 'Permission (decimal) for creating/copying folders. 0755 => 593, 0777 => 511', + 'description': 'Can be either decimal (493) or octal (leading zero: 0755)', }, { 'name': 'permission_file', - 'default': 0755, + 'default': '0755', 'label': 'File CHMOD', 'description': 'Same as Folder CHMOD but for files', }, diff --git a/couchpotato/core/_base/_core/getppid.py b/couchpotato/core/_base/_core/getppid.py deleted file mode 100644 index 6854e291..00000000 --- a/couchpotato/core/_base/_core/getppid.py +++ /dev/null @@ -1,45 +0,0 @@ -from ctypes import * -from ctypes.wintypes import * -import win32process - - -class PROCESSENTRY32(Structure): - _fields_ = ( - ('dwSize', DWORD,), - ('cntUsage', DWORD,), - ('th32ProcessID', DWORD,), - ('th32DefaultHeapID', POINTER(ULONG),), - ('th32ModuleID', DWORD,), - ('cntThreads', DWORD,), - ('th32ParentProcessID', DWORD,), - ('pcPriClassBase', LONG,), - ('dwFlags', DWORD,), - ('szExeFile', c_char * MAX_PATH,), - ) - - -def getppid(pid): - """the Windows version of os.getppid""" - pe = PROCESSENTRY32() - pe.dwSize = sizeof(PROCESSENTRY32) - - snapshot = windll.kernel32.CreateToolhelp32Snapshot(2, 0) - try: - if not windll.kernel32.Process32First(snapshot, byref(pe)): - raise WindowsError - while pe.th32ProcessID != pid: - if not windll.kernel32.Process32Next(snapshot, byref(pe)): - raise WindowsError - result = pe.th32ParentProcessID - finally: - windll.kernel32.CloseHandle(snapshot) - - if result not in win32process.EnumProcesses(): - result = 1 - - return result - - -import os -if not hasattr(os, 'getppid'): - os.getppid = getppid diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py index 7a142081..b22f9ba5 100644 --- a/couchpotato/core/_base/_core/main.py +++ b/couchpotato/core/_base/_core/main.py @@ -12,21 +12,26 @@ import time import traceback import webbrowser -if os.name == 'nt': - import getppid - - log = CPLog(__name__) + class Core(Plugin): ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown'] shutdown_started = False def __init__(self): - addApiView('app.shutdown', self.shutdown) - addApiView('app.restart', self.restart) - addApiView('app.available', self.available) + addApiView('app.shutdown', self.shutdown, docs = { + 'desc': 'Shutdown the app.', + 'return': {'type': 'string: shutdown'} + }) + addApiView('app.restart', self.restart, docs = { + 'desc': 'Restart the app.', + 'return': {'type': 'string: restart'} + }) + addApiView('app.available', self.available, docs = { + 'desc': 'Check if app available.' + }) addEvent('app.crappy_shutdown', self.crappyShutdown) addEvent('app.crappy_restart', self.crappyRestart) @@ -51,6 +56,9 @@ class Core(Plugin): }) def crappyShutdown(self): + if self.shutdown_started: + return + try: self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False) return True @@ -59,6 +67,9 @@ class Core(Plugin): return False def crappyRestart(self): + if self.shutdown_started: + return + try: self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False) return True @@ -77,6 +88,7 @@ class Core(Plugin): def initShutdown(self, restart = False): if self.shutdown_started: log.info('Already shutting down') + return log.info('Shutting down' if not restart else 'Restarting') diff --git a/couchpotato/core/_base/scheduler/main.py b/couchpotato/core/_base/scheduler/main.py index 896e1cae..fb3b01ee 100644 --- a/couchpotato/core/_base/scheduler/main.py +++ b/couchpotato/core/_base/scheduler/main.py @@ -15,7 +15,7 @@ class Scheduler(Plugin): def __init__(self): - logging.getLogger('apscheduler').setLevel(logging.WARNING) + logging.getLogger('apscheduler').setLevel(logging.ERROR) addEvent('schedule.cron', self.cron) addEvent('schedule.interval', self.interval) diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py index 6e9236f6..8bf26359 100644 --- a/couchpotato/core/_base/updater/main.py +++ b/couchpotato/core/_base/updater/main.py @@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from git.repository import LocalRepository +from datetime import datetime import os import time import traceback @@ -29,18 +30,34 @@ class Updater(Plugin): addEvent('app.load', self.check) - addApiView('updater.info', self.getInfo) + addApiView('updater.info', self.getInfo, docs = { + 'desc': 'Get updater information', + 'return': { + 'type': 'object', + 'example': """{ + 'repo_name': "Name of used repository", + 'last_check': "last checked for update", + 'update_version': "available update version or empty", + 'version': current_cp_version +}"""} + }) addApiView('updater.update', self.doUpdateView) - addApiView('updater.check', self.checkView) + addApiView('updater.check', self.checkView, docs = { + 'desc': 'Check for available update', + 'return': {'type': 'see updater.info'} + }) def getInfo(self): - return jsonified({ + return jsonified(self.info()) + + def info(self): + return { 'repo_name': self.repo_name, 'last_check': self.last_check, 'update_version': self.update_version, 'version': self.getVersion() - }) + } def getVersion(self): @@ -78,14 +95,14 @@ class Updater(Plugin): log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8])) if local.getDate() < remote.getDate(): + self.update_version = { + 'hash': remote.hash[:8], + 'date': remote.getDate(), + } if self.conf('automatic') and not self.update_failed: if self.doUpdate(): fireEventAsync('app.crappy_restart') else: - self.update_version = { - 'hash': remote.hash[:8], - 'date': remote.getDate(), - } if self.conf('notification'): fireEvent('updater.available', message = 'A new update is available', data = self.getVersion()) @@ -106,11 +123,16 @@ class Updater(Plugin): self.repo.saveStash() log.info('Updating to latest version') + info = self.info() self.repo.pull() # Delete leftover .pyc files self.deletePyc() + # Notify before returning and restarting + version_date = datetime.fromtimestamp(info['update_version']['date']) + fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info) + return True except: log.error('Failed updating via GIT: %s' % traceback.format_exc()) diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js index 48d077c1..202540d3 100644 --- a/couchpotato/core/_base/updater/static/updater.js +++ b/couchpotato/core/_base/updater/static/updater.js @@ -76,32 +76,10 @@ var UpdaterBase = new Class({ Api.request('updater.update', { 'onComplete': function(json){ - if(json.success){ - App.restart(); - - $(document.body).set('spin', { - 'message': 'Updating' - }); - $(document.body).spin(); - - var checks = 0; - var interval = 0; - interval = setInterval(function(){ - Api.request('', { - 'onSuccess': function(){ - if(checks > 2){ - clearInterval(interval); - $(document.body).unspin(); - self.info(); - } - } - }); - checks++; - }, 500) - + App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating'); + App.checkAvailable.delay(500, App); } - } }); } diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index e90f951a..39ee6129 100644 --- a/couchpotato/core/downloaders/base.py +++ b/couchpotato/core/downloaders/base.py @@ -18,9 +18,12 @@ class Downloader(Plugin): def download(self, data = {}): pass - def createFileName(self, data, filename, movie): - name = os.path.join('%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))) - if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filename: + def createNzbName(self, data, movie): + return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie)) + + def createFileName(self, data, filedata, movie): + name = os.path.join(self.createNzbName(data, movie)) + if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filedata: return '%s.%s' % (name, 'rar') return '%s.%s' % (name, data.get('type')) @@ -30,10 +33,10 @@ class Downloader(Plugin): return '' - def isCorrectType(self, type): - is_correct = type in self.type + def isCorrectType(self, item_type): + is_correct = item_type in self.type if not is_correct: log.debug("Downloader doesn't support this type") - return bool + return is_correct diff --git a/couchpotato/core/downloaders/blackhole/__init__.py b/couchpotato/core/downloaders/blackhole/__init__.py index a958ecd1..3231c48d 100644 --- a/couchpotato/core/downloaders/blackhole/__init__.py +++ b/couchpotato/core/downloaders/blackhole/__init__.py @@ -5,6 +5,7 @@ def start(): config = [{ 'name': 'blackhole', + 'order': 30, 'groups': [ { 'tab': 'downloaders', diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 083f312d..b65e2511 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/couchpotato/core/downloaders/nzbget/main.py @@ -22,7 +22,7 @@ class NZBGet(Downloader): log.info('Sending "%s" to NZBGet.' % data.get('name')) url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} - nzb_name = data.get('name') + '.nzb' + nzb_name = '%s.nzb' % self.createNzbName(data, movie) rpc = xmlrpclib.ServerProxy(url) try: diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index d2b47786..41ac9206 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -39,7 +39,7 @@ class Sabnzbd(Downloader): 'apikey': self.conf('api_key'), 'cat': self.conf('category'), 'mode': 'addurl', - 'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), + 'nzbname': self.createNzbName(data, movie), } if isfunction(data.get('download')): diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index 6f588481..88c15454 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -1,8 +1,7 @@ from axl.axel import Event -from couchpotato.core.helpers.variable import mergeDicts +from couchpotato.core.helpers.variable import mergeDicts, natcmp from couchpotato.core.logger import CPLog import threading -import time import traceback log = CPLog(__name__) @@ -36,7 +35,7 @@ def addEvent(name, handler, priority = 100): return h - e.handle(handler, priority = priority) + e.handle(createHandle, priority = priority) def removeEvent(name, handler): e = events[name] @@ -84,7 +83,8 @@ def fireEvent(name, *args, **kwargs): results = None # Loop over results, stop when first not None result is found. - for r in result: + for r_key in sorted(result.iterkeys(), cmp = natcmp): + r = result[r_key] if r[0] is True and r[1] is not None: results = r[1] break @@ -95,7 +95,8 @@ def fireEvent(name, *args, **kwargs): else: results = [] - for r in result: + for r_key in sorted(result.iterkeys(), cmp = natcmp): + r = result[r_key] if r[0] == True and r[1]: results.append(r[1]) elif r[1]: diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 09ee6b8f..a9dff599 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -70,7 +70,7 @@ def jsonify(mimetype, *args, **kwargs): def jsonified(*args, **kwargs): from couchpotato.environment import Env - callback = getParam('json_callback', None) + callback = getParam('callback_func', None) if callback: return padded_jsonify(callback, *args, **kwargs) else: diff --git a/couchpotato/core/loader.py b/couchpotato/core/loader.py index a5816cc2..ee7afcb1 100644 --- a/couchpotato/core/loader.py +++ b/couchpotato/core/loader.py @@ -51,8 +51,13 @@ class Loader(object): did_save += self.loadSettings(m, module_name, save = False) self.loadPlugins(m, plugin.get('name')) - except ImportError: - log.debug('Import error, remove the empty folder: %s' % plugin.get('module')) + except ImportError as e: + # todo:: subclass ImportError for missing requirements. + if (e.message.lower().startswith("missing")): + log.error(e.message) + pass + # todo:: this needs to be more descriptive. + log.error('Import error, remove the empty folder: %s' % plugin.get('module')) except: log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc())) diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 9dd3cfe3..254059e0 100644 --- a/couchpotato/core/notifications/base.py +++ b/couchpotato/core/notifications/base.py @@ -23,14 +23,15 @@ class Notification(Plugin): # Attach listeners for listener in self.listen_to: if not listener in self.dont_listen_to: + addEvent(listener, self.createNotifyHandler(listener)) - # Add on snatch default - def notify(message, data): - if not self.conf('on_snatch', default = 1) and listener == 'movie.snatched': - return - return self.notify(message = message, data = data) + def createNotifyHandler(self, listener): + def notify(message, data): + if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': + return + return self.notify(message = message, data = data) - addEvent(listener, notify) + return notify def notify(self, message = '', data = {}): pass diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py index a5a478b2..c3a16214 100644 --- a/couchpotato/core/notifications/core/main.py +++ b/couchpotato/core/notifications/core/main.py @@ -1,8 +1,13 @@ +from couchpotato import get_session from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.request import jsonified +from couchpotato.core.helpers.encoding import toUnicode +from couchpotato.core.helpers.request import jsonified, getParam +from couchpotato.core.helpers.variable import tryInt from couchpotato.core.logger import CPLog from couchpotato.core.notifications.base import Notification +from couchpotato.core.settings.model import Notification as Notif +from sqlalchemy.sql.expression import or_ import time log = CPLog(__name__) @@ -11,28 +16,104 @@ log = CPLog(__name__) class CoreNotifier(Notification): messages = [] + listen_to = [ + 'movie.downloaded', 'movie.snatched', + 'updater.available', 'updater.updated', + ] def __init__(self): + super(CoreNotifier, self).__init__() addEvent('notify', self.notify) addEvent('notify.frontend', self.frontend) - addApiView('core_notifier.listener', self.listener) + addApiView('notification.markread', self.markAsRead, docs = { + 'desc': 'Mark notifications as read', + 'params': { + 'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'}, + }, + }) + + addApiView('notification.list', self.listView, docs = { + 'desc': 'Get list of notifications', + 'params': { + 'limit_offset': {'desc': 'Limit and offset the notification list. Examples: "50" or "50,30"'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any notification returned or not, + 'notifications': array, notifications found, +}"""} + }) + + addApiView('notification.listener', self.listener) self.registerEvents() - def registerEvents(self): # Library update, frontend refresh addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data)) - def notify(self, message = '', data = {}): - self.add(data = { - 'message': message, - 'raw': data, + def markAsRead(self): + ids = getParam('ids').split(',') + + db = get_session() + + q = db.query(Notif) \ + .filter(or_(*[Notif.id == tryInt(s) for s in ids])) + q.update({Notif.read: True}) + + db.commit() + + return jsonified({ + 'success': True }) + def listView(self): + + db = get_session() + limit_offset = getParam('limit_offset', None) + + q = db.query(Notif) + + if limit_offset: + splt = limit_offset.split(',') + limit = splt[0] + offset = 0 if len(splt) is 1 else splt[1] + q = q.limit(limit).offset(offset) + + results = q.all() + notifications = [] + for n in results: + ndict = n.to_dict() + ndict['type'] = 'notification' + notifications.append(ndict) + + return jsonified({ + 'success': True, + 'empty': len(notifications) == 0, + 'notifications': notifications + }) + + def notify(self, message = '', data = {}): + + db = get_session() + + n = Notif( + message = toUnicode(message), + data = data + ) + db.add(n) + db.commit() + + ndict = n.to_dict() + ndict['type'] = 'notification' + ndict['time'] = time.time() + self.messages.append(ndict) + + db.remove() + def frontend(self, type = 'notification', data = {}): self.messages.append({ 'time': time.time(), @@ -48,6 +129,18 @@ class CoreNotifier(Notification): if message['time'] > (time.time() - 15): messages.append(message) + # Get unread + if getParam('init'): + db = get_session() + + notifications = db.query(Notif) \ + .filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \ + .all() + for n in notifications: + ndict = n.to_dict() + ndict['type'] = 'notification' + messages.append(ndict) + self.messages = [] return jsonified({ 'success': True, diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js index e96fdba2..371b95ed 100644 --- a/couchpotato/core/notifications/core/static/notification.js +++ b/couchpotato/core/notifications/core/static/notification.js @@ -10,19 +10,86 @@ var NotificationBase = new Class({ // Listener App.addEvent('load', self.startInterval.bind(self)); App.addEvent('unload', self.stopTimer.bind(self)); - self.addEvent('notification', self.notify.bind(self)) + App.addEvent('notification', self.notify.bind(self)); // Add test buttons to settings page App.addEvent('load', self.addTestButtons.bind(self)); + // Notification bar + self.notifications = [] + App.addEvent('load', function(){ + + App.block.notification = new Block.Menu(self, { + 'class': 'notification_menu', + 'onOpen': self.markAsRead.bind(self) + }) + $(App.block.notification).inject(App.getBlock('search'), 'after'); + self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide(); + + /* App.getBlock('notification').addLink(new Element('a.more', { + 'href': App.createUrl('notifications'), + 'text': 'Show older notifications' + })); */ + }) + + }, + + notify: function(result){ + var self = this; + + var added = new Date(); + added.setTime(result.added*1000) + + result.el = App.getBlock('notification').addLink( + new Element('span.'+(result.read ? 'read' : '' )).adopt( + new Element('span.message', {'text': result.message}), + new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) + ) + , 'top'); + self.notifications.include(result); + + if(!result.read) + self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) + + }, + + setBadge: function(value){ + var self = this; + self.badge.set('text', value) + self.badge[value ? 'show' : 'hide']() + }, + + markAsRead: function(){ + var self = this; + + var rn = self.notifications.filter(function(n){ + return !n.read + }) + + var ids = [] + rn.each(function(n){ + ids.include(n.id) + }) + + if(ids.length > 0) + Api.request('notification.markread', { + 'data': { + 'ids': ids.join(',') + }, + 'onSuccess': function(){ + self.setBadge('') + } + }) + }, startInterval: function(){ var self = this; - self.request = Api.request('core_notifier.listener', { + self.request = Api.request('notification.listener', { 'initialDelay': 100, 'delay': 3000, + 'data': {'init':true}, 'onSuccess': self.processData.bind(self) }) @@ -40,16 +107,12 @@ var NotificationBase = new Class({ this.request.stopTimer() }, - notify: function(data){ - var self = this; - - }, - processData: function(json){ var self = this; + self.request.options.data = {} Array.each(json.result, function(result){ - App.fireEvent(result.type, result.data) + App.fireEvent(result.type, result) }) }, diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 7bd6056d..29ade280 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/couchpotato/core/notifications/growl/main.py @@ -34,7 +34,7 @@ class Growl(Notification): except: log.error('Failed register of growl: %s' % traceback.format_exc()) - def notify(self, type = '', message = '', data = {}): + def notify(self, message = '', data = {}): if self.isDisabled(): return self.register() diff --git a/couchpotato/core/notifications/notifymywp/__init__.py b/couchpotato/core/notifications/notifymywp/__init__.py new file mode 100644 index 00000000..76228e6a --- /dev/null +++ b/couchpotato/core/notifications/notifymywp/__init__.py @@ -0,0 +1,43 @@ +from .main import NotifyMyWP + +def start(): + return NotifyMyWP() + +config = [{ + 'name': 'notifymywp', + 'groups': [ + { + 'tab': 'notifications', + 'name': 'notifymywp', + 'label': 'Notify My Windows Phone', + 'options': [ + { + 'name': 'enabled', + 'default': 0, + 'type': 'enabler', + }, + { + 'name': 'api_key', + 'description': 'Multiple keys seperated by a comma. Maximum of 5.' + }, + { + 'name': 'dev_key', + 'advanced': True, + }, + { + 'name': 'priority', + 'default': 0, + 'type': 'dropdown', + 'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)], + }, + { + 'name': 'on_snatch', + 'default': 0, + 'type': 'bool', + 'advanced': True, + 'description': 'Also send message when movie is snatched.', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py new file mode 100644 index 00000000..7c294bfc --- /dev/null +++ b/couchpotato/core/notifications/notifymywp/main.py @@ -0,0 +1,23 @@ +from couchpotato.core.logger import CPLog +from couchpotato.core.notifications.base import Notification +from pynmwp import PyNMWP + +log = CPLog(__name__) + + +class NotifyMyWP(Notification): + + def notify(self, message = '', data = {}): + if self.isDisabled(): return + + keys = self.conf('api_key').split(',') + p = PyNMWP(keys, self.conf('dev_key')) + + response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) + + for key in keys: + if not response[key]['Code'] == u'200': + log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s' % (key, response[key]['message'])) + return False + + return response diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 4fe0a45c..2651caec 100644 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -30,7 +30,7 @@ class XBMC(Notification): } try: - self.urlopen(url, headers = headers) + self.urlopen(url, headers = headers, show_error = False) except: log.error("Couldn't sent command to XBMC") return False diff --git a/couchpotato/core/plugins/automation/__init__.py b/couchpotato/core/plugins/automation/__init__.py index 550da5d7..8f7d80b7 100644 --- a/couchpotato/core/plugins/automation/__init__.py +++ b/couchpotato/core/plugins/automation/__init__.py @@ -5,6 +5,7 @@ def start(): config = [{ 'name': 'automation', + 'order': 30, 'groups': [ { 'tab': 'automation', diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 49b73228..41802e82 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -98,7 +98,7 @@ class Plugin(object): if not headers.get('Referer'): headers['Referer'] = urlparse(url).hostname if not headers.get('User-Agent'): - headers['User-Agent'] = '' + headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2' host = urlparse(url).hostname self.wait(host) diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py index ca7e3134..887edc30 100644 --- a/couchpotato/core/plugins/browser/main.py +++ b/couchpotato/core/plugins/browser/main.py @@ -6,12 +6,31 @@ import os import string if os.name == 'nt': - import win32file + import imp + try: + imp.find_module('win32file') + except: + # todo:: subclass ImportError for missing dependencies, vs. broken plugins? + raise ImportError("Missing the win32file module, which is a part of the prerequisite \ + pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/"); + else: + import win32file class FileBrowser(Plugin): def __init__(self): - addApiView('directory.list', self.view) + addApiView('directory.list', self.view, docs = { + 'desc': 'Return the directory list of a given directory', + 'params': { + 'path': {'desc': 'The directory to scan'}, + 'show_hidden': {'desc': 'Also show hidden files'} + }, + 'return': {'type': 'object', 'example': """{ + 'is_root': bool, //is top most folder + 'empty': bool, //directory is empty + 'dirs': array, //directory names +}"""} + }) def getDirectories(self, path = '/', show_hidden = True): @@ -27,7 +46,7 @@ class FileBrowser(Plugin): if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)): dirs.append(p + os.path.sep) - return dirs + return sorted(dirs) def getFiles(self): pass diff --git a/couchpotato/core/plugins/file/main.py b/couchpotato/core/plugins/file/main.py index dad6bdd3..e0f02408 100644 --- a/couchpotato/core/plugins/file/main.py +++ b/couchpotato/core/plugins/file/main.py @@ -19,7 +19,13 @@ class FileManager(Plugin): addEvent('file.download', self.download) addEvent('file.types', self.getTypes) - addApiView('file.cache/', self.showCacheFile, static = True) + addApiView('file.cache/', self.showCacheFile, static = True, docs = { + 'desc': 'Return a file from the cp_data/cache directory', + 'params': { + 'filename': {'desc': 'path/filename of the wanted file'} + }, + 'return': {'type': 'file'} + }) def showCacheFile(self, filename = ''): diff --git a/couchpotato/core/plugins/file/static/file.js b/couchpotato/core/plugins/file/static/file.js index 50458db6..2093e2fe 100644 --- a/couchpotato/core/plugins/file/static/file.js +++ b/couchpotato/core/plugins/file/static/file.js @@ -2,7 +2,7 @@ var File = new Class({ initialize: function(file){ var self = this; - + if(!file){ self.el = new Element('div'); return @@ -17,7 +17,7 @@ var File = new Class({ createImage: function(){ var self = this; - + var file_name = self.data.path.replace(/^.*[\\\/]/, ''); self.el = new Element('div', { diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index 4ceca115..9a4cde04 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Library, LibraryTitle, File from string import ascii_letters -import json import traceback log = CPLog(__name__) @@ -75,7 +74,7 @@ class LibraryPlugin(Plugin): library.tagline = toUnicode(info.get('tagline', '')) library.year = info.get('year', 0) library.status_id = done_status.get('id') - library.info = toUnicode(json.dumps(info)) + library.info = info db.commit() # Titles @@ -85,6 +84,8 @@ class LibraryPlugin(Plugin): titles = info.get('titles', []) log.debug('Adding titles: %s' % titles) for title in titles: + if not title: + continue t = LibraryTitle( title = toUnicode(title), simple_title = self.simplifyTitle(title), diff --git a/couchpotato/core/plugins/log/main.py b/couchpotato/core/plugins/log/main.py index 00d2baf0..2a2c022f 100644 --- a/couchpotato/core/plugins/log/main.py +++ b/couchpotato/core/plugins/log/main.py @@ -12,9 +12,27 @@ log = CPLog(__name__) class Logging(Plugin): def __init__(self): - addApiView('logging.get', self.get) - addApiView('logging.clear', self.clear) - addApiView('logging.log', self.log) + addApiView('logging.get', self.get, docs = { + 'desc': 'Get the full log file by number', + 'params': { + 'nr': {'desc': 'Number of the log to get.'} + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'log': string, //Log file + 'total': int, //Total log files available +}"""} + }) + addApiView('logging.clear', self.clear, docs = { + 'desc': 'Remove all the log files' + }) + addApiView('logging.log', self.log, docs = { + 'desc': 'Get the full log file by number', + 'params': { + 'type': {'desc': 'Type of logging, default "error"'}, + '**kwargs': {'type':'object', 'desc': 'All other params will be printed in the log string.'}, + } + }) def get(self): diff --git a/couchpotato/core/plugins/log/static/log.css b/couchpotato/core/plugins/log/static/log.css index 6a4eef56..ec1f838c 100644 --- a/couchpotato/core/plugins/log/static/log.css +++ b/couchpotato/core/plugins/log/static/log.css @@ -42,7 +42,7 @@ float: left; width: 86%; line-height: 150%; - padding: 3px 1%; + padding: 3px 0; border-top: 1px solid rgba(255, 255, 255, 0.2); font-size: 11px; font-family: Lucida Console, Monaco, Nimbus Mono L; @@ -56,10 +56,10 @@ .page.log .container .time { clear: both; - width: 11%; + width: 14%; color: lightgrey; padding: 3px 0; } - + .page.log .container .time:last-child { display: none; } diff --git a/couchpotato/core/plugins/log/static/log.js b/couchpotato/core/plugins/log/static/log.js index 0bbb2682..1668ded6 100644 --- a/couchpotato/core/plugins/log/static/log.js +++ b/couchpotato/core/plugins/log/static/log.js @@ -4,6 +4,20 @@ Page.Log = new Class({ name: 'log', title: 'Show recent logs.', + has_tab: false, + + initialize: function(options){ + var self = this; + self.parent(options) + + + App.getBlock('more').addLink(new Element('a', { + 'href': App.createUrl(self.name), + 'text': self.name.capitalize(), + 'title': self.title + })) + + }, indexAction: function(){ var self = this; diff --git a/couchpotato/core/plugins/manage/__init__.py b/couchpotato/core/plugins/manage/__init__.py index 4aa9fe80..30f6ea68 100644 --- a/couchpotato/core/plugins/manage/__init__.py +++ b/couchpotato/core/plugins/manage/__init__.py @@ -7,10 +7,9 @@ config = [{ 'name': 'manage', 'groups': [ { - 'tab': 'renamer', + 'tab': 'manage', 'label': 'movie library manager', 'description': 'Add your existing movie folders.', - 'wizard': True, 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/manage/main.py b/couchpotato/core/plugins/manage/main.py index 6810199f..20fdd4c3 100644 --- a/couchpotato/core/plugins/manage/main.py +++ b/couchpotato/core/plugins/manage/main.py @@ -21,7 +21,12 @@ class Manage(Plugin): fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2) addEvent('manage.update', self.updateLibrary) - addApiView('manage.update', self.updateLibraryView) + addApiView('manage.update', self.updateLibraryView, docs = { + 'desc': 'Update the library by scanning for new movies', + 'params': { + 'full': {'desc': 'Do a full update or just recently changed/added movies.'}, + } + }) if not Env.get('dev'): addEvent('app.load', self.updateLibrary) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 44045ba9..433ecaa9 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -26,14 +26,60 @@ class MoviePlugin(Plugin): } def __init__(self): - addApiView('movie.search', self.search) - addApiView('movie.list', self.listView) - addApiView('movie.refresh', self.refresh) + addApiView('movie.search', self.search, docs = { + 'desc': 'Search the movie providers for a movie', + 'params': { + 'q': {'desc': 'The (partial) movie name you want to search for'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.list', self.listView, docs = { + 'desc': 'List movies in wanted list', + 'params': { + 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, + 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, + 'search': {'desc': 'Search movie title'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any movies returned or not, + 'movies': array, movies found, +}"""} + }) + addApiView('movie.refresh', self.refresh, docs = { + 'desc': 'Refresh a movie by id', + 'params': { + 'id': {'desc': 'The id of the movie that needs to be refreshed'}, + } + }) addApiView('movie.available_chars', self.charView) - - addApiView('movie.add', self.addView) - addApiView('movie.edit', self.edit) - addApiView('movie.delete', self.delete) + addApiView('movie.add', self.addView, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, + 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.edit', self.edit, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, + 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, + 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + addApiView('movie.delete', self.delete, docs = { + 'desc': 'Delete a movie from the wanted list', + 'params': { + 'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'}, + } + }) addEvent('movie.add', self.add) addEvent('movie.get', self.get) @@ -55,19 +101,11 @@ class MoviePlugin(Plugin): if not isinstance(status, (list, tuple)): status = [status] - q = db.query(Movie) \ .join(Movie.library, Library.titles) \ - .options(joinedload_all('releases.status')) \ - .options(joinedload_all('releases.quality')) \ - .options(joinedload_all('releases.files')) \ - .options(joinedload_all('releases.info')) \ - .options(joinedload_all('library.titles')) \ - .options(joinedload_all('library.files')) \ - .options(joinedload_all('status')) \ - .options(joinedload_all('files')) \ .filter(LibraryTitle.default == True) \ - .filter(or_(*[Movie.status.has(identifier = s) for s in status])) + .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \ + .group_by(Movie.id) filter_or = [] if starts_with: @@ -88,17 +126,32 @@ class MoviePlugin(Plugin): q = q.order_by(asc(LibraryTitle.simple_title)) + q = q.subquery() + q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \ + .options(joinedload_all('releases')) \ + .options(joinedload_all('profile.types')) \ + .options(joinedload_all('library.titles')) \ + .options(joinedload_all('library.files')) \ + .options(joinedload_all('status')) \ + .options(joinedload_all('files')) \ + + if limit_offset: splt = limit_offset.split(',') limit = splt[0] offset = 0 if len(splt) is 1 else splt[1] - q = q.limit(limit).offset(offset) + q2 = q2.limit(limit).offset(offset) - results = q.all() + results = q2.all() movies = [] for movie in results: - temp = movie.to_dict(self.default_dict) + temp = movie.to_dict({ + 'profile': {'types': {}}, + 'releases': {'files':{}, 'info': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {}, + }) movies.append(temp) return movies @@ -117,7 +170,8 @@ class MoviePlugin(Plugin): .join(Movie.library, Library.titles) \ .options(joinedload_all('library.titles')) \ .filter(LibraryTitle.default == True) \ - .filter(or_(*[Movie.status.has(identifier = s) for s in status])) + .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \ + .group_by(Movie.id) results = q.all() @@ -230,6 +284,13 @@ class MoviePlugin(Plugin): db.commit() + # Remove releases + available_status = fireEvent('status.get', 'available', single = True) + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + movie_dict = m.to_dict(self.default_dict) if force_readd or do_search: @@ -255,19 +316,31 @@ class MoviePlugin(Plugin): params = getParams() db = get_session() - m = db.query(Movie).filter_by(id = params.get('id')).first() - m.profile_id = params.get('profile_id') + available_status = fireEvent('status.get', 'available', single = True) - # Default title - for title in m.library.titles: - title.default = params.get('default_title').lower() == title.title.lower() + ids = params.get('id').split(',') + for movie_id in ids: - db.commit() + m = db.query(Movie).filter_by(id = movie_id).first() + m.profile_id = params.get('profile_id') - fireEvent('movie.restatus', m.id) + # Remove releases + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() - movie_dict = m.to_dict(self.default_dict) - fireEventAsync('searcher.single', movie_dict) + # Default title + if params.get('default_title'): + for title in m.library.titles: + title.default = params.get('default_title').lower() == title.title.lower() + + db.commit() + + fireEvent('movie.restatus', m.id) + + movie_dict = m.to_dict(self.default_dict) + fireEventAsync('searcher.single', movie_dict) return jsonified({ 'success': True, @@ -280,9 +353,11 @@ class MoviePlugin(Plugin): status = fireEvent('status.add', 'deleted', single = True) - movie = db.query(Movie).filter_by(id = params.get('id')).first() - movie.status_id = status.get('id') - db.commit() + ids = params.get('id').split(',') + for movie_id in ids: + movie = db.query(Movie).filter_by(id = movie_id).first() + movie.status_id = status.get('id') + db.commit() return jsonified({ 'success': True, diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js index e90fb89f..d70db1f5 100644 --- a/couchpotato/core/plugins/movie/static/list.js +++ b/couchpotato/core/plugins/movie/static/list.js @@ -3,8 +3,9 @@ var MovieList = new Class({ Implements: [Options], options: { - navigation: false, - limit: 50 + navigation: true, + limit: 50, + menu: [] }, movies: [], @@ -69,18 +70,23 @@ var MovieList = new Class({ self.scrollspy.stop(); } - Object.each(movies, function(info){ + Object.each(movies, function(movie){ // Attach proper actions - var a = self.options.actions - var actions = a[info.status.identifier.capitalize()] || a.Wanted || {}; + var a = self.options.actions, + status = Status.get(movie.status_id); + var actions = a[status.identifier.capitalize()] || a.Wanted || {}; var m = new Movie(self, { - 'actions': actions - }, info); + 'actions': actions, + 'view': self.current_view, + 'onSelect': self.calculateSelected.bind(self) + }, movie); $(m).inject(self.movie_list); m.fireEvent('injected'); + self.movies.include(m) + }); }, @@ -89,8 +95,12 @@ var MovieList = new Class({ var self = this; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + self.current_view = self.getSavedView(); + self.el.addClass(self.current_view+'_list') + self.navigation = new Element('div.alph_nav').adopt( - self.alpha = new Element('ul.inlay', { + self.navigation_actions = new Element('ul.inlay.actions.reversed'), + self.navigation_alpha = new Element('ul.numbers', { 'events': { 'click:relay(li)': function(e, el){ self.movie_list.empty() @@ -99,24 +109,74 @@ var MovieList = new Class({ } } }), - self.search_input = new Element('input.inlay', { + self.navigation_search_input = new Element('input.inlay', { 'placeholder': 'Search', 'events': { 'keyup': self.search.bind(self), 'change': self.search.bind(self) } - })/*, - self.view = new Element('ul.inlay').adopt( - new Element('li.list'), - new Element('li.thumbnails'), - new Element('li.text') - )*/ + }), + self.navigation_menu = new Block.Menu(self), + self.mass_edit_form = new Element('div.mass_edit_form').adopt( + new Element('span.select').adopt( + self.mass_edit_select = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': self.massEditToggleAll.bind(self) + } + }), + self.mass_edit_selected = new Element('span.count', {'text': 0}), + self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) + ), + new Element('div.quality').adopt( + self.mass_edit_quality = new Element('select'), + new Element('a.button.orange', { + 'text': 'Change quality', + 'events': { + 'click': self.changeQualitySelected.bind(self) + } + }) + ), + new Element('div.delete').adopt( + new Element('span[text=or]'), + new Element('a.button.red', { + 'text': 'Delete', + 'events': { + 'click': self.deleteSelected.bind(self) + } + }) + ) + ) ).inject(self.el, 'top'); + // Mass edit + self.mass_edit_select_class = new Form.Check(self.mass_edit_select); + Quality.getActiveProfiles().each(function(profile){ + new Element('option', { + 'value': profile.id ? profile.id : profile.data.id, + 'text': profile.label ? profile.label : profile.data.label + }).inject(self.mass_edit_quality) + }); + + // Actions + ['mass_edit', 'thumbs', 'list'].each(function(view){ + self.navigation_actions.adopt( + new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', { + 'events': { + 'click': function(e){ + var a = 'active'; + self.navigation_actions.getElements('.'+a).removeClass(a); + self.changeView(this.get('data-view')); + this.addClass(a); + } + } + }).adopt(new Element('span')) + ) + }); + // All self.letters['all'] = new Element('li.letter_all.available.active', { 'text': 'ALL', - }).inject(self.alpha); + }).inject(self.navigation_alpha); // Chars chars.split('').each(function(c){ @@ -124,7 +184,7 @@ var MovieList = new Class({ 'text': c, 'class': 'letter_'+c, 'data-letter': c - }).inject(self.alpha); + }).inject(self.navigation_alpha); }); // Get available chars and highlight @@ -141,12 +201,126 @@ var MovieList = new Class({ } }); + // Add menu or hide + if (self.options.menu.length > 0) + self.options.menu.each(function(menu_item){ + self.navigation_menu.addLink(menu_item); + }) + else + self.navigation_menu.hide() + + self.nav_scrollspy = new ScrollSpy({ + min: 10, + onEnter: function(){ + self.navigation.addClass('float') + }, + onLeave: function(){ + self.navigation.removeClass('float') + } + }); + + }, + + calculateSelected: function(){ + var self = this; + + var selected = 0, + movies = self.movies.length; + self.movies.each(function(movie){ + selected += movie.isSelected() ? 1 : 0 + }) + + var indeterminate = selected > 0 && selected < movies, + checked = selected == movies && selected > 0; + + self.mass_edit_select.set('indeterminate', indeterminate) + + self.mass_edit_select_class[checked ? 'check' : 'uncheck']() + self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate') + + self.mass_edit_selected.set('text', selected); + }, + + deleteSelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + var qObj = new Question('Are you sure you want to delete the selected movies?', 'Items using this profile, will be set to the default quality.', [{ + 'text': 'Yes, delete them', + 'class': 'delete', + 'events': { + 'click': function(e){ + (e).preventDefault(); + Api.request('movie.delete', { + 'data': { + 'id': ids.join(',') + }, + 'onSuccess': function(){ + qObj.close(); + + self.movies.each(function(movie){ + if (movie.isSelected()){ + $(movie).destroy() + self.movies.erase(movie) + } + }); + + self.calculateSelected() + } + }); + + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + changeQualitySelected: function(){ + var self = this; + var ids = self.getSelectedMovies() + + Api.request('movie.edit', { + 'data': { + 'id': ids.join(','), + 'profile_id': self.mass_edit_quality.get('value') + }, + 'onSuccess': self.search.bind(self) + }); + }, + + getSelectedMovies: function(){ + var self = this; + + var ids = [] + self.movies.each(function(movie){ + if (movie.isSelected()) + ids.include(movie.get('id')) + }); + + return ids + }, + + massEditToggleAll: function(){ + var self = this; + + var select = self.mass_edit_select.get('checked'); + + self.movies.each(function(movie){ + movie.select(select) + }); + + self.calculateSelected() }, reset: function(){ var self = this; - self.navigation.getElements('.active').removeClass('active') + self.movies = [] + self.calculateSelected() + self.navigation_alpha.getElements('.active').removeClass('active') self.offset = 0; self.load_more.show(); self.scrollspy.start(); @@ -162,12 +336,32 @@ var MovieList = new Class({ }, + changeView: function(new_view){ + var self = this; + + self.movies.each(function(movie){ + movie.changeView(new_view) + }); + + self.el + .removeClass(self.current_view+'_list') + .addClass(new_view+'_list') + + self.current_view = new_view; + Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000}); + }, + + getSavedView: function(){ + var self = this; + return Cookie.read(self.options.identifier+'_view') || 'thumbs'; + }, + search: function(){ var self = this; if(self.search_timer) clearTimeout(self.search_timer); self.search_timer = (function(){ - var search_value = self.search_input.get('value'); + var search_value = self.navigation_search_input.get('value'); if (search_value == self.last_search_value) return self.reset() @@ -211,8 +405,8 @@ var MovieList = new Class({ loadMore: function(){ var self = this; - - self.getMovies() + if(self.offset >= self.options.limit) + self.getMovies() }, store: function(movies){ diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 7d06d709..55dce2e3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -1,35 +1,59 @@ -/* @override - http://localhost:5000/static/movie_plugin/movie.css - http://192.168.1.20:5000/static/movie_plugin/movie.css - http://127.0.0.1:5000/static/movie_plugin/movie.css -*/ - .movies { - padding: 20px 0; + padding: 60px 0 20px; } + .movies.mass_edit_list { + padding-top: 90px; + } + .movies .movie { position: relative; border-radius: 4px; margin: 10px 0; - overflow: hidden; + width: 100%; + transition: all 0.2s linear; } - .movies .movie:hover { - border-color: #ddd #fff #fff #ddd; + .movies .movie.list_view, .movies .movie.mass_edit_view { + margin: 1px 0; + border-radius: 0; + background: no-repeat; + box-shadow: none; + border-bottom: 1px solid rgba(255,255,255,0.05); } + .movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover { + background: rgba(255,255,255,0.03); + } + .movies .data { padding: 20px; - height: 140px; - width: 800px; + height: 180px; + width: 840px; position: relative; float: right; - border-radius: 0; - -moz-border-radius: 0; - -webkit-border-radius: 0; overflow: hidden; + transition: all 0.2s linear; } + .movies .list_view .data, .movies .mass_edit_view .data { + height: 30px; + padding: 3px 0 3px 10px; + width: 938px; + box-shadow: none; + border: 0; + background: none; + } + + .movies .movie .check { + display: none; + } + + .movies.mass_edit_list .movie .check { + float: left; + display: block; + margin: 7px 0 0 5px; + } + .movies .poster { float: left; width: 120px; @@ -37,21 +61,37 @@ overflow: hidden; height: 180px; border-radius: 4px 0 0 4px; - + transition: all 0.2s linear; + } + .movies .list_view .poster, .movies .mass_edit_view .poster { + width: 20px; + height: 30px; + } + .movies.mass_edit_list .poster { + display: none; + } + .movies .poster img, .options .poster img { width: 101%; height: 101%; } - + .movies .info .title { font-size: 30px; font-weight: bold; margin-bottom: 10px; float: left; - width: 80%; + width: 90%; + transition: all 0.2s linear; } - + .movies .list_view .info .title, .movies .mass_edit_view .info .title { + font-size: 16px; + font-weight: normal; + text-overflow: ellipsis; + width: 64%; + } + .movies .info .year { font-size: 30px; margin-bottom: 10px; @@ -59,8 +99,13 @@ color: #bbb; width: 10%; text-align: right; + transition: all 0.2s linear; } - + .movies .list_view .info .year, .movies .mass_edit_view .info .year { + font-size: 16px; + width: 6%; + } + .movies .info .rating { font-size: 30px; margin-bottom: 10px; @@ -69,7 +114,7 @@ width: 5%; padding: 0 0 0 3%; } - + .movies .info .description { clear: both; height: 80px; @@ -78,35 +123,85 @@ .movies .data:hover .description { overflow: auto; } - - .movies .data .quality span { - padding: 0 5px; - font-weight: bold; + .movies .list_view .info .description, .movies .mass_edit_view .info .description { + display: none; + } + + .movies .data .quality { + display: block; + min-height: 20px; + vertical-align: mid; } - .movies .data .quality span:first-child {padding-left: 0;} - - .movies .data .quality .available { color: orange; } - .movies .data .quality .snatched { color: lightgreen; } - + + .movies .data .quality span { + padding: 2px 3px; + font-weight: bold; + opacity: 0.5; + font-size: 10px; + height: 16px; + line-height: 12px; + vertical-align: middle; + display: inline-block; + text-transform: uppercase; + text-shadow: none; + font-weight: normal; + margin: 0 2px; + border-radius: 2px; + background-color: rgba(255,255,255,0.1); + } + .movies .list_view .data .quality, .movies .mass_edit_view .data .quality { + text-align: right; + float: right; + width: 30%; + } + + .movies .data .quality .available, .movies .data .quality .snatched { + opacity: 1; + box-shadow: 1px 1px 0 rgba(0,0,0,0.2); + cursor: pointer; + } + + .movies .data .quality .available { background-color: #578bc3; } + .movies .data .quality .snatched { background-color: #369545; } + .movies .data .quality .done { + background-color: #369545; + opacity: 1; + } + .movies .data .quality .finish { + background-image: url('../images/sprite.png'); + background-repeat: no-repeat; + background-position: 0 2px; + padding-left: 14px; + background-size: 14px + } + .movies .data .actions { line-height: 0; clear: both; float: right; margin-top: -25px; } - .movies .data:hover .action { opacity: 0.6; } - .movies .data:hover .action:hover { opacity: 1; } - + .movies .data:hover .action { opacity: 0.6; } + .movies .data:hover .action:hover { opacity: 1; } + .movies.mass_edit_list .data .actions { + display: none; + } + .movies .data .action { background-repeat: no-repeat; background-position: center; display: inline-block; - width: 20px; - height: 20px; + width: 26px; + height: 26px; padding: 3px; opacity: 0; } - + + .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { + margin: -34px 2px 0 0; + background: #4e5969; + } + .movies .delete_container { clear: both; text-align: center; @@ -128,22 +223,23 @@ color: #fff; background-color: #d32917; } - + .movies .options { position: absolute; margin-left: 120px; + width: 840px; } - + .movies .options .form { margin: 70px 20px 0; float: left; font-size: 20px; } - + .movies .options .form select { margin-right: 20px; } - + .movies .options .table { height: 180px; overflow: auto; @@ -151,18 +247,27 @@ .movies .options .table .item { border-bottom: 1px solid rgba(255,255,255,0.1); } + .movies .options .table .item.ignored span { + text-decoration: line-through; + color: rgba(255,255,255,0.4); + text-shadow: none; + } + .movies .options .table .item.ignored .delete { + background-image: url('../images/icon.undo.png'); + } + .movies .options .table .item:last-child { border: 0; } - .movies .options .table .item:nth-child(even) { + .movies .options .table .item:nth-child(even) { background: rgba(255,255,255,0.05); } - .movies .options .table .item:not(.head):hover { + .movies .options .table .item:not(.head):hover { background: rgba(255,255,255,0.03); } - + .movies .options .table .item > * { display: inline-block; padding: 0 5px; - width: 50px; + width: 60px; min-height: 24px; white-space: nowrap; text-overflow: ellipsis; @@ -174,21 +279,21 @@ border: 0; } .movies .options .table .provider { - width: 120px; + width: 130px; } .movies .options .table .name { - width: 360px; + width: 370px; overflow: hidden; text-align: left; padding: 0 10px; } - .movies .options .table.files .name { width: 598px; } - .movies .options .table .type { width: 120px; } - .movies .options .table .is_available { width: 80px; } - + .movies .options .table.files .name { width: 605px; } + .movies .options .table .type { width: 130px; } + .movies .options .table .is_available { width: 90px; } + .movies .options .table a { - width: 16px !important; - height: 16px; + width: 30px !important; + height: 20px; opacity: 0.8; } .movies .options .table a:hover { @@ -202,7 +307,7 @@ padding-bottom: 4px; height: auto; } - + .movies .load_more { display: block; padding: 10px; @@ -214,10 +319,23 @@ } .movies .alph_nav { - overflow: hidden; + transition: box-shadow .4s linear; + position: fixed; + z-index: 2; + top: 0; + padding: 100px 60px 7px; + width: 1080px; + margin: 0 -60px; + box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); } - -.movies .alph_nav ul { + + .movies .alph_nav.float { + box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5); + border-radius: 0; + background: #4e5969; + } + +.movies .alph_nav ul.numbers, .movies .alph_nav ul.actions { list-style: none; padding: 0 0 1px; margin: 0; @@ -225,30 +343,129 @@ user-select: none; } - .movies .alph_nav li { + .movies .alph_nav .numbers li, .movies .alph_nav .actions li { display: inline-block; vertical-align: top; - width: 24px; + width: 22px; height: 24px; line-height: 26px; text-align: center; cursor: pointer; - color: #666; + color: rgba(255,255,255,0.2); border: 1px solid transparent; + transition: all 0.1s ease-in-out; + text-shadow: none; } - .movies .alph_nav li:first-child { - width: 34px; - } - .movies .alph_nav li.active, .movies .alph_nav li:hover { - font-weight: bolder; - color: #fff; + .movies .alph_nav .numbers li:first-child { + width: 43px; + margin-left: 7px; } .movies .alph_nav li.available { - color: #fff; + color: rgba(255,255,255,0.8); + font-weight: bolder; + } - + .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { + color: #fff; + font-size: 24px; + line-height: 24px; + } + .movies .alph_nav input { padding: 6px 5px; - margin: 0; - float: right; - } \ No newline at end of file + margin: 0 0 0 6px; + float: left; + width: 155px; + height: 25px; + } + + .movies .alph_nav .actions { + margin: 0 6px 0 0; + -moz-user-select: none; + } + .movies .alph_nav .actions li { + border-radius: 1px; + width: auto; + } + .movies .alph_nav .actions li.active { + background: none; + border: 1px solid transparent; + box-shadow: none; + } + .movies .alph_nav .actions li span { + display: block; + background: url('../images/sprite.png') no-repeat; + width: 25px; + height: 100%; + } + + .movies .alph_nav .actions li.mass_edit span { + background-position: 3px 3px; + } + + .movies .alph_nav .actions li.list span { + background-position: 3px -95px; + } + + .movies .alph_nav .actions li.thumbs span { + background-position: 3px -74px; + } + + .movies .alph_nav .actions li:first-child { + border-radius: 3px 0 0 3px; + } + .movies .alph_nav .actions li:last-child { + border-radius: 0 3px 3px 0; + } + + .movies .alph_nav .mass_edit_form { + clear: both; + text-align: center; + display: none; + } + .movies.mass_edit_list .mass_edit_form { + display: block; + } + .movies.mass_edit_list .mass_edit_form .select { + float: left; + margin: 5px 0 0 5px; + font-size: 14px; + } + .movies.mass_edit_list .mass_edit_form .select span { + vertical-align: middle; + opacity: 0.7; + } + .movies.mass_edit_list .mass_edit_form .select .count { + font-weight: bold; + margin: 0 3px 0 10px; + } + + .movies .alph_nav .mass_edit_form .quality { + float: left; + padding: 8px 0 0; + margin: 0 0 0 16px; + } + .movies .alph_nav .mass_edit_form .quality select { + width: 120px; + margin-right: 5px; + } + .movies .alph_nav .mass_edit_form .button { + padding: 3px 7px; + } + + .movies .alph_nav .mass_edit_form .delete { + float: left; + padding: 8px 0 0 8px; + } + + .movies .alph_nav .mass_edit_form .delete span { + margin: 0 10px 0 0; + } + + .movies .alph_nav .more_menu { + margin-left: 48px; + } + + .movies .alph_nav .more_menu > a { + background-position: center -157px; + } diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js index 4b7ac27c..dac9bf98 100644 --- a/couchpotato/core/plugins/movie/static/movie.js +++ b/couchpotato/core/plugins/movie/static/movie.js @@ -8,6 +8,7 @@ var Movie = new Class({ var self = this; self.data = data; + self.view = options.view || 'thumbs'; self.profile = Quality.getProfile(data.profile_id) || {}; self.parent(self, options); @@ -17,6 +18,13 @@ var Movie = new Class({ var self = this; self.el = new Element('div.movie.inlay').adopt( + self.select_checkbox = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': function(){ + self.fireEvent('select') + } + } + }), self.thumbnail = File.Select.single('poster', self.data.library.files), self.data_container = new Element('div.data.inlay.light', { 'tween': { @@ -38,12 +46,23 @@ var Movie = new Class({ self.description = new Element('div.description', { 'text': self.data.library.plot }), - self.quality = new Element('div.quality') + self.quality = new Element('div.quality', { + 'events': { + 'click': function(e){ + var releases = self.el.getElement('.actions .releases'); + if(releases) + releases.fireEvent('click', [e]) + } + } + }) ), self.actions = new Element('div.actions') ) ); + self.changeView(self.view); + self.select_checkbox_class = new Form.Check(self.select_checkbox); + // Add profile if(self.profile.data) self.profile.getTypes().each(function(type){ @@ -57,11 +76,13 @@ var Movie = new Class({ // Add done releases Array.each(self.data.releases, function(release){ - var q = self.quality.getElement('.q_'+ release.quality.identifier); - if(!q) - var q = self.addQuality(release.quality_id) + var q = self.quality.getElement('.q_id'+ release.quality_id), + status = Status.get(release.status_id); - q.addClass(release.status.identifier); + if(!q && status.identifier == 'snatched') + var q = self.addQuality(release.quality_id) + if (q) + q.addClass(status.identifier); }); @@ -82,7 +103,7 @@ var Movie = new Class({ var q = Quality.getQuality(quality_id); return new Element('span', { 'text': q.label, - 'class': 'q_'+q.identifier + 'class': 'q_'+q.identifier + ' q_id' + q.id }).inject(self.quality); }, @@ -97,18 +118,30 @@ var Movie = new Class({ }).pop() if(title) - return title.title + return self.getUnprefixedTitle(title.title) else if(titles.length > 0) - return titles[0].title + return self.getUnprefixedTitle(titles[0].title) return 'Unknown movie' }, + getUnprefixedTitle: function(t){ + if(t.substr(0, 4).toLowerCase() == 'the ') + t = t.substr(4) + ', The'; + return t; + }, + slide: function(direction, el){ var self = this; if(direction == 'in'){ - self.el.addEvent('outerClick', self.slide.bind(self, 'out')) + self.temp_view = self.view; + self.changeView('thumbs') + + self.el.addEvent('outerClick', function(){ + self.changeView(self.temp_view) + self.slide('out') + }) el.show(); self.data_container.tween('right', 0, -840); } @@ -123,8 +156,27 @@ var Movie = new Class({ } }, + changeView: function(new_view){ + var self = this; + + self.el + .removeClass(self.view+'_view') + .addClass(new_view+'_view') + + self.view = new_view; + }, + get: function(attr){ return this.data[attr] || this.data.library[attr] + }, + + select: function(bool){ + var self = this; + self.select_checkbox_class[bool ? 'check' : 'uncheck']() + }, + + isSelected: function(){ + return this.select_checkbox.get('checked'); } }); @@ -180,7 +232,7 @@ var IMDBAction = new Class({ gotoIMDB: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); window.open('http://www.imdb.com/title/'+self.id+'/'); } @@ -208,7 +260,7 @@ var ReleaseAction = new Class({ show: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( @@ -226,11 +278,15 @@ var ReleaseAction = new Class({ ).inject(self.release_container) Array.each(self.movie.data.releases, function(release){ + + var status = Status.get(release.status_id), + quality = Quality.getProfile(release.quality_id) + new Element('div', { - 'class': 'item ' + release.status.identifier + 'class': 'item ' + status.identifier }).adopt( new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), - new Element('span.quality', {'text': release.quality.label}), + new Element('span.quality', {'text': quality.get('label')}), new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}), new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.score', {'text': self.get(release, 'score')}), @@ -238,7 +294,7 @@ var ReleaseAction = new Class({ new Element('a.download.icon', { 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); self.download(release); } } @@ -246,9 +302,9 @@ var ReleaseAction = new Class({ new Element('a.delete.icon', { 'events': { 'click': function(e){ - (e).stop(); - self.del(release); - this.getParent('.item').destroy(); + (e).preventDefault(); + self.ignore(release); + this.getParent('.item').toggleClass('ignored') } } }) @@ -278,10 +334,10 @@ var ReleaseAction = new Class({ }); }, - del: function(release){ + ignore: function(release){ var self = this; - Api.request('release.delete', { + Api.request('release.ignore', { 'data': { 'id': release.id } diff --git a/couchpotato/core/plugins/movie/static/search.css b/couchpotato/core/plugins/movie/static/search.css index 6d34c72c..4f43ffe1 100644 --- a/couchpotato/core/plugins/movie/static/search.css +++ b/couchpotato/core/plugins/movie/static/search.css @@ -1,73 +1,100 @@ -/* @override - http://localhost:5000/static/movie_plugin/search.css - http://192.168.1.20:5000/static/movie_plugin/search.css - http://127.0.0.1:5000/static/movie_plugin/search.css -*/ - .search_form { display: inline-block; + vertical-align: middle; width: 25%; } .search_form input { - padding-right: 25px; - padding: 4px; + padding: 4px 20px 4px 4px; margin: 0; font-size: 14px; - width: 90%; + width: 100%; + height: 24px; } + .search_form input:focus { + padding-right: 83px; + } + + .search_form .input .enter { + background: #369545 url('../images/sprite.png') right -188px no-repeat; + padding: 0 20px 0 4px; + border-radius: 2px; + text-transform: uppercase; + font-size: 10px; + margin-left: -78px; + display: inline-block; + opacity: 0; + position: relative; + top: -2px; + cursor: pointer; + vertical-align: middle; + visibility: hidden; + } + .search_form.focused .input .enter { + visibility: visible; + } + .search_form.focused.filled .input .enter { + opacity: 1; + } + .search_form .input a { width: 17px; height: 20px; display: inline-block; - margin: 0 0 -5px -20px; + margin: -2px 0 0 2px; top: 4px; right: 5px; - background: url('../images/checks.png') right -36px no-repeat; + background: url('../images/sprite.png') left -37px no-repeat; cursor: pointer; + opacity: 0; + transition: all 0.2s ease-in-out; + vertical-align: middle; + } + + .search_form.filled .input a { + opacity: 1; } .search_form .results_container { position: absolute; background: #5c697b; - margin: 6px 0 0 -246px; + margin: 6px 0 0 -230px; width: 470px; min-height: 140px; - border-radius: 3px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - - box-shadow: 0 0 50px rgba(0,0,0,0.55); + box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); + display: none; } - .search_form .spinner { - background: rgba(0,0,0,0.8) url('../images/spinner.gif') no-repeat center 70px; - } - - .search_form .pointer { - border-right: 10px solid transparent; - border-left: 10px solid transparent; - border-bottom: 10px solid #5c697b; + .search_form.shown.filled .results_container { display: block; - position: absolute; - width: 0px; - left: 50%; - margin: -9px 0 0 110px; + } + + .search_form .results_container:before { + content: ' '; + height: 0; + position: relative; + width: 0; + border: 10px solid transparent; + border-bottom-color: #5c697b; + display: block; + top: -20px; + left: 346px; } .search_form .results { - max-height: 550px; + max-height: 570px; overflow-x: hidden; padding: 10px 0; + margin-top: -18px; } .movie_result { overflow: hidden; - min-height: 140px; + height: 140px; } .movie_result .options { - height: 139px; + height: 140px; border: 1px solid transparent; border-width: 1px 0; border-radius: 0; @@ -106,12 +133,11 @@ .movie_result .data { padding: 0 15px; - width: 440px; + width: 470px; position: relative; - min-height: 100px; + height: 140px; top: 0; - margin: -143px 0 0 0; - min-height: 140px; + margin: -140px 0 0 0; background: #5c697b; cursor: pointer; @@ -120,7 +146,7 @@ } .movie_result:last-child .data { border-bottom: 0; } - + .movie_result .in_wanted, .movie_result .in_library { position: absolute; margin-top: 105px; @@ -131,16 +157,17 @@ display: inline-block; margin: 15px 3% 15px 0; vertical-align: top; - border-radius: 3px; box-shadow: 0 0 3px rgba(0,0,0,0.35); } .movie_result .info { - width: 74%; + width: 80%; display: inline-block; vertical-align: top; padding: 15px 0; + height: 120px; + overflow: hidden; } .movie_result .info .tagline { @@ -164,3 +191,7 @@ .movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:after { content: ")"; } + +.search_form .mask { + border-radius: 3px; +} \ No newline at end of file diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js index 3f29d139..53a8d399 100644 --- a/couchpotato/core/plugins/movie/static/search.js +++ b/couchpotato/core/plugins/movie/static/search.js @@ -10,12 +10,23 @@ Block.Search = new Class({ self.el = new Element('div.search_form').adopt( new Element('div.input').adopt( self.input = new Element('input.inlay', { - 'placeholder': 'Search for new movies', + 'placeholder': 'Search & add a new movie', 'events': { 'keyup': self.keyup.bind(self), - 'focus': self.hideResults.bind(self, false) + 'focus': function(){ + self.el.addClass('focused') + }, + 'blur': function(){ + self.el.removeClass('focused') + } } }), + new Element('span.enter', { + 'events': { + 'click': self.keyup.bind(self) + }, + 'text':'Enter' + }), new Element('a', { 'events': { 'click': self.clear.bind(self) @@ -32,24 +43,24 @@ Block.Search = new Class({ } } }).adopt( - new Element('div.pointer'), self.results = new Element('div.results') - ).hide() + ) ); - self.spinner = new Spinner(self.result_container); + self.mask = new Element('div.mask').inject(self.result_container).fade('hide'); }, clear: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); self.input.set('value', ''); self.input.focus() self.movies = [] self.results.empty() + self.el.removeClass('filled') }, hideResults: function(bool){ @@ -57,7 +68,7 @@ Block.Search = new Class({ if(self.hidden == bool) return; - self.result_container[bool ? 'hide' : 'show'](); + self.el[bool ? 'removeClass' : 'addClass']('shown'); if(bool){ History.removeEvent('change', self.hideResults.bind(self, !bool)); @@ -74,16 +85,14 @@ Block.Search = new Class({ keyup: function(e){ var self = this; - if(['up', 'down'].indexOf(e.key) > -1){ - p('select item') - } - else if(self.q() != self.last_q) { + self.el[self.q() ? 'addClass' : 'removeClass']('filled') + + if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click')) self.autocomplete() - } }, - autocomplete: function(delay){ + autocomplete: function(){ var self = this; if(!self.q()){ @@ -91,10 +100,7 @@ Block.Search = new Class({ return } - self.spinner.show() - - if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer) - self.autocomplete_timer = self.list.delay((delay || 300), self) + self.list() }, list: function(){ @@ -105,9 +111,14 @@ Block.Search = new Class({ var q = self.q(); var cache = self.cache[q]; - self.hideResults(false) + self.hideResults(false); if(!cache){ + self.positionMask().fade('in'); + + if(!self.spinner) + self.spinner = createSpinner(self.mask); + self.api_request = Api.request('movie.search', { 'data': { 'q': q @@ -125,7 +136,7 @@ Block.Search = new Class({ fill: function(q, json){ var self = this; - self.spinner.hide(); + self.positionMask() self.cache[q] = json self.movies = {} @@ -138,10 +149,30 @@ Block.Search = new Class({ self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m }); - + if(q != self.q()) self.list() + // Calculate result heights + var w = window.getSize(), + rc = self.result_container.getCoordinates(); + + self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px') + self.mask.hide() + + }, + + positionMask: function(){ + var self = this; + + var s = self.result_container.getSize() + + return self.mask.setStyles({ + 'width': s.x, + 'height': s.y + }).position({ + 'relativeTo': self.result_container + }) }, loading: function(bool){ @@ -252,7 +283,9 @@ Block.Search.Item = new Class({ add: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); + + self.loadingMask(); Api.request('movie.add', { 'data': { @@ -260,8 +293,6 @@ Block.Search.Item = new Class({ 'title': self.title_select.get('value'), 'profile_id': self.profile_select.get('value') }, - 'useSpinner': true, - 'spinnerTarget': self.options, 'onComplete': function(){ self.options.empty(); self.options.adopt( @@ -293,10 +324,9 @@ Block.Search.Item = new Class({ }) : null, self.info.in_wanted ? new Element('span.in_wanted', { 'text': 'Already in wanted list: ' + self.info.in_wanted.label - }) : null, - self.info.in_library ? new Element('span.in_library', { + }) : (self.info.in_library ? new Element('span.in_library', { 'text': 'Already in library: ' + self.info.in_library.label - }) : null, + }) : null), self.title_select = new Element('select', { 'name': 'title' }), @@ -318,7 +348,7 @@ Block.Search.Item = new Class({ }).inject(self.title_select) }) - Object.each(Quality.getActiveProfiles(), function(profile){ + Quality.getActiveProfiles().each(function(profile){ new Element('option', { 'value': profile.id ? profile.id : profile.data.id, 'text': profile.label ? profile.label : profile.data.label @@ -330,6 +360,25 @@ Block.Search.Item = new Class({ }, + loadingMask: function(){ + var self = this; + + var s = self.options.getSize(); + + self.mask = new Element('span.mask', { + 'styles': { + 'width': s.x, + 'height': s.y + } + }).inject(self.options).fade('hide').position({ + 'relativeTo': self.options + }) + + createSpinner(self.mask) + self.mask.fade('in') + + }, + toElement: function(){ return this.el } diff --git a/couchpotato/core/plugins/profile/static/profile.css b/couchpotato/core/plugins/profile/static/profile.css index c1f57196..9d50d2fd 100644 --- a/couchpotato/core/plugins/profile/static/profile.css +++ b/couchpotato/core/plugins/profile/static/profile.css @@ -1,5 +1,3 @@ -/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */ - .add_new_profile { padding: 20px; display: block; @@ -18,7 +16,7 @@ padding: 14px; background-position: center; } - + .profile .qualities { min-height: 80px; } @@ -42,36 +40,36 @@ .profile .wait_for input { margin: 0 5px !important; } - + .profile .types { padding: 0; margin: 0 20px 0 -4px; display: inline-block; } - + .profile .types li { padding: 3px 5px; border-bottom: 1px solid rgba(255,255,255,0.2); list-style: none; } .profile .types li:last-child { border: 0; } - + .profile .types li > * { display: inline-block; vertical-align: middle; line-height: 0; margin-right: 10px; } - + .profile .quality_type select { width: 186px; margin-left: -1px; } - + .profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle { visibility: hidden; } - + .profile .types .type .handle { background: url('./handle.png') center; display: inline-block; @@ -82,7 +80,7 @@ cursor: -webkit-grab; margin: 0; } - + .profile .types .type .delete { background-position: left center; height: 20px; @@ -90,13 +88,13 @@ visibility: hidden; cursor: pointer; } - + .profile .types .type:hover:not(.is_empty) .delete { visibility: visible; } - + #profile_ordering { - + } #profile_ordering ul { @@ -114,19 +112,19 @@ padding: 0 5px; } #profile_ordering li:last-child { border: 0; } - + #profile_ordering li .check { margin: 2px 10px 0 0; vertical-align: top; } - + #profile_ordering li > span { display: inline-block; height: 20px; vertical-align: top; - line-height: 20px; + line-height: 20px; } - + #profile_ordering li .handle { background: url('./handle.png') center; width: 20px; diff --git a/couchpotato/core/plugins/profile/static/profile.js b/couchpotato/core/plugins/profile/static/profile.js index 7a6571e1..470fda8f 100644 --- a/couchpotato/core/plugins/profile/static/profile.js +++ b/couchpotato/core/plugins/profile/static/profile.js @@ -156,7 +156,7 @@ var Profile = new Class({ 'class': 'delete', 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); Api.request('profile.delete', { 'data': { 'id': self.data.id diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 892fefc6..5736ddd7 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -16,7 +16,7 @@ log = CPLog(__name__) class QualityPlugin(Plugin): qualities = [ - {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']}, + {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, @@ -155,6 +155,10 @@ class QualityPlugin(Plugin): log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file)) return self.setCache(hash, quality) + for tag in quality.get('tags', []): + if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words): + return self.setCache(hash, quality) + if list(set(quality.get('tags', [])) & set(words)): log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file)) return self.setCache(hash, quality) diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/plugins/quality/static/quality.css index e2081738..f71f007e 100644 --- a/couchpotato/core/plugins/quality/static/quality.css +++ b/couchpotato/core/plugins/quality/static/quality.css @@ -1,7 +1,5 @@ -/* @override http://127.0.0.1:5000/static/quality_plugin/quality.css */ - .group_sizes { - + } .group_sizes .head { @@ -17,7 +15,7 @@ .group_sizes .label { max-width: 120px; } - + .group_sizes .min, .group_sizes .max { text-align: center; width: 50px; diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/plugins/quality/static/quality.js index 87a2b5a1..bd2ff2ac 100644 --- a/couchpotato/core/plugins/quality/static/quality.js +++ b/couchpotato/core/plugins/quality/static/quality.js @@ -39,10 +39,10 @@ var QualityBase = new Class({ self.settings = App.getPage('Settings') self.settings.addEvent('create', function(){ - var tab = self.settings.createTab('profile', { + var tab = self.settings.createSubTab('profile', { 'label': 'Quality', 'name': 'profile' - }); + }, self.settings.tabs.searcher ,'searcher'); self.tab = tab.tab; self.content = tab.content; @@ -93,7 +93,7 @@ var QualityBase = new Class({ var data = data || {'id': randomString()} var profile = new Profile(data) self.profiles.include(profile) - + return profile; }, diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 9297dbb7..f83b322f 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -16,8 +16,24 @@ class Release(Plugin): def __init__(self): addEvent('release.add', self.add) - addApiView('release.download', self.download) - addApiView('release.delete', self.delete) + addApiView('release.download', self.download, docs = { + 'desc': 'Send a release manually to the downloaders', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) + addApiView('release.delete', self.delete, docs = { + 'desc': 'Delete releases', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) + addApiView('release.ignore', self.ignore, docs = { + 'desc': 'Toggle ignore, for bad or wrong releases', + 'params': { + 'id': {'type': 'id', 'desc': 'ID of the release object in release-table'} + } + }) def add(self, group): db = get_session() @@ -95,6 +111,22 @@ class Release(Plugin): 'success': True }) + def ignore(self): + + db = get_session() + id = getParam('id') + + rel = db.query(Relea).filter_by(id = id).first() + if rel: + ignored_status = fireEvent('status.get', 'ignored', single = True) + available_status = fireEvent('status.get', 'available', single = True) + rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') + db.commit() + + return jsonified({ + 'success': True + }) + def download(self): db = get_session() diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index ab2cd899..b1b53395 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/couchpotato/core/plugins/renamer/__init__.py @@ -24,6 +24,7 @@ rename_options = { config = [{ 'name': 'renamer', + 'order': 40, 'description': 'Move and rename your downloaded movies to your movie directory.', 'groups': [ { diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index b67214f7..e8d70188 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -22,7 +22,9 @@ class Renamer(Plugin): def __init__(self): - addApiView('renamer.scan', self.scanView) + addApiView('renamer.scan', self.scanView, docs = { + 'desc': 'For the renamer to check for new files to rename', + }) addEvent('renamer.scan', self.scan) addEvent('app.load', self.scan) @@ -31,7 +33,7 @@ class Renamer(Plugin): def scanView(self): - fireEvent('renamer.scan') + fireEventAsync('renamer.scan') return jsonified({ 'success': True @@ -65,6 +67,8 @@ class Renamer(Plugin): nfo_name = self.conf('nfo_name') separator = self.conf('separator') + db = get_session() + for group_identifier in groups: group = groups[group_identifier] @@ -123,7 +127,7 @@ class Renamer(Plugin): # Move nfo depending on settings if file_type is 'nfo' and not self.conf('rename_nfo'): log.debug('Skipping, renaming of %s disabled' % file_type) - if self.conf('clean_up'): + if self.conf('cleanup'): for current_file in group['files'][file_type]: remove_files.append(current_file) continue @@ -223,7 +227,6 @@ class Renamer(Plugin): cd += 1 # Before renaming, remove the lower quality files - db = get_session() library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() done_status = fireEvent('status.get', 'done', single = True) @@ -308,11 +311,26 @@ class Renamer(Plugin): if isinstance(src, File): src = src.path - log.info('(fake) Removing "%s"' % src) + log.info('Removing "%s"' % src) + try: + os.remove(src) + except: + log.error('Failed removing %s: %s', (src, traceback.format_exc())) # Remove matching releases for release in remove_releases: - log.info('(fake) Removing release %s' % release.identifier) + log.debug('Removing release %s' % release.identifier) + try: + db.delete(release) + except: + log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc())) + + if group['dirname'] and group['parentdir']: + try: + log.info('Deleting folder: %s' % group['parentdir']) + self.deleteEmptyFolder(group['parentdir']) + except: + log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc())) # Search for trailers etc fireEventAsync('renamer.after', group) @@ -378,3 +396,20 @@ class Renamer(Plugin): def replaceDoubles(self, string): return string.replace(' ', ' ').replace(' .', '.') + + def deleteEmptyFolder(self, folder): + + for root, dirs, files in os.walk(folder): + + for dir_name in dirs: + full_path = os.path.join(root, dir_name) + if len(os.listdir(full_path)) == 0: + try: + os.rmdir(full_path) + except: + log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc())) + + try: + os.rmdir(folder) + except: + log.error('Couldn\'t remove empty directory %s: %s' % (folder, traceback.format_exc())) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index a389bb9a..2d9607cb 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/couchpotato/core/plugins/scanner/main.py @@ -43,7 +43,7 @@ class Scanner(Plugin): 'trailer': ('video', 'trailer'), 'nfo': ('nfo', 'nfo'), 'movie': ('video', 'movie'), - 'movie': ('movie', 'movie_extra'), + 'movie_extra': ('movie', 'movie_extra'), 'backdrop': ('image', 'backdrop'), 'leftover': ('leftover', 'leftover'), } @@ -365,7 +365,6 @@ class Scanner(Plugin): def getMeta(self, filename): try: - p = enzyme.parse(filename) return { 'video': p.video[0].codec, @@ -377,6 +376,8 @@ class Scanner(Plugin): log.debug('Failed to parse meta for %s' % filename) except NoParserError: log.debug('No parser found for %s' % filename) + except: + log.debug('Failed parsing %s' % filename) return {} diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index ee7c9806..49dbba41 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ - sizeScore + sizeScore, providerScore, duplicateScore log = CPLog(__name__) @@ -31,4 +31,10 @@ class Score(Plugin): except: pass + # Provider score + score += providerScore(nzb['provider']) + + # Duplicates in name + score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title']) + return score diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index ccf993ec..1a2f2b88 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -27,14 +27,14 @@ def nameScore(name, year): score = 0 name = name.lower() - #give points for the cool stuff + # give points for the cool stuff for value in name_scores: v = value.split(':') add = int(v.pop()) if v.pop() in name: score = score + add - #points if the year is correct + # points if the year is correct if str(year) in name: score = score + 5 @@ -58,3 +58,24 @@ def nameRatioScore(nzb_name, movie_name): def sizeScore(size): return 0 if size else -20 + + +def providerScore(provider): + if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']: + return 30 + + if provider in ['Newznab', 'Moovee', 'X264']: + return 10 + + return 0 + + +def duplicateScore(nzb_name, movie_name): + + nzb_words = re.split('\W+', simplifyString(nzb_name)) + movie_words = re.split('\W+', simplifyString(movie_name)) + + # minus for duplicates + duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1] + + return len(list(set(duplicates) - set(movie_words))) * -4 diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py index 14a43768..8e0f2cf9 100644 --- a/couchpotato/core/plugins/searcher/__init__.py +++ b/couchpotato/core/plugins/searcher/__init__.py @@ -6,6 +6,7 @@ def start(): config = [{ 'name': 'searcher', + 'order': 20, 'groups': [ { 'tab': 'searcher', diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index 4b4ee339..f3dc28fa 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -33,13 +33,19 @@ class Searcher(Plugin): ).all() for movie in movies: - - self.single(movie.to_dict({ + movie_dict = movie.to_dict({ 'profile': {'types': {'quality': {}}}, 'releases': {'status': {}, 'quality': {}}, 'library': {'titles': {}, 'files':{}}, 'files': {} - })) + }) + + try: + self.single(movie_dict) + except IndexError: + fireEvent('library.update', movie_dict['library']['identifier'], force = True) + except: + log.error('Search failed for %s: %s' % (movie_dict['library']['identifier'], traceback.format_exc())) # Break if CP wants to shut down if self.shuttingDown(): diff --git a/couchpotato/core/plugins/status/main.py b/couchpotato/core/plugins/status/main.py index 5256f8de..9912fee2 100644 --- a/couchpotato/core/plugins/status/main.py +++ b/couchpotato/core/plugins/status/main.py @@ -20,6 +20,7 @@ class StatusPlugin(Plugin): 'wanted': 'Wanted', 'snatched': 'Snatched', 'deleted': 'Deleted', + 'ignored': 'Ignored', } def __init__(self): @@ -29,7 +30,13 @@ class StatusPlugin(Plugin): addEvent('status.all', self.all) addEvent('app.initialize', self.fill) - addApiView('status.list', self.list) + addApiView('status.list', self.list, docs = { + 'desc': 'Check for available update', + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'list': array, statuses +}"""} + }) def list(self): diff --git a/couchpotato/core/plugins/status/static/status.js b/couchpotato/core/plugins/status/static/status.js index 7967ac58..2b8d30f3 100644 --- a/couchpotato/core/plugins/status/static/status.js +++ b/couchpotato/core/plugins/status/static/status.js @@ -5,7 +5,13 @@ var StatusBase = new Class({ self.statuses = statuses; - } + }, + + get: function(id){ + return this.statuses.filter(function(status){ + return status.id == id + }).pick() + }, }); window.Status = new StatusBase(); diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index 88728bd9..903e934e 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/couchpotato/core/plugins/subtitle/__init__.py @@ -8,7 +8,9 @@ config = [{ 'groups': [ { 'tab': 'renamer', + 'subtab': 'subtitles', 'name': 'subtitle', + 'label': 'Download subtitles after rename', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py index 49b0cb9e..033df088 100644 --- a/couchpotato/core/plugins/trailer/__init__.py +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -7,8 +7,10 @@ config = [{ 'name': 'trailer', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'trailer', 'name': 'trailer', + 'label': 'Download trailer after rename', 'options': [ { 'name': 'enabled', diff --git a/couchpotato/core/plugins/userscript/bookmark.js b/couchpotato/core/plugins/userscript/bookmark.js new file mode 100644 index 00000000..5ee8c376 --- /dev/null +++ b/couchpotato/core/plugins/userscript/bookmark.js @@ -0,0 +1,43 @@ +var includes = {{includes|tojson}}; +var excludes = {{excludes|tojson}}; + +var specialChars = '\\{}+.():-|^$'; +var makeRegex = function(pattern) { + pattern = pattern.split(''); + var i, len = pattern.length; + for( i = 0; i < len; i++) { + var character = pattern[i]; + if(specialChars.indexOf(character) > -1) { + pattern[i] = '\\' + character; + } else if(character === '?') { + pattern[i] = '.'; + } else if(character === '*') { + pattern[i] = '.*'; + } + } + return new RegExp('^' + pattern.join('') + '$'); +}; + +var isCorrectUrl = function() { + for(i in includes) { + var reg = includes[i] + if (makeRegex(reg).test(document.location.href)) + return true; + } + return false; +} +var addUserscript = function() { + // Add window param + document.body.setAttribute('cp_auto_open', true) + + // Load userscript + var e = document.createElement('script'); + e.setAttribute('type', 'text/javascript'); + e.setAttribute('charset', 'UTF-8'); + e.setAttribute('src', '{{host}}couchpotato.js?r=' + Math.random() * 99999999); + document.body.appendChild(e) +} +if(isCorrectUrl()) + addUserscript() +else + alert('Can\'t find a proper movie on this page..') diff --git a/couchpotato/core/plugins/userscript/main.py b/couchpotato/core/plugins/userscript/main.py index 569f36de..359fd552 100644 --- a/couchpotato/core/plugins/userscript/main.py +++ b/couchpotato/core/plugins/userscript/main.py @@ -1,4 +1,3 @@ -from couchpotato import index from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.helpers.request import getParam, jsonified @@ -8,6 +7,7 @@ from couchpotato.core.plugins.base import Plugin from couchpotato.environment import Env from flask.globals import request from flask.helpers import url_for +from flask.templating import render_template import os log = CPLog(__name__) @@ -15,14 +15,27 @@ log = CPLog(__name__) class Userscript(Plugin): + version = 2 + def __init__(self): - addApiView('userscript.get/', self.getUserScript, static = True) + addApiView('userscript.get//', self.getUserScript, static = True) addApiView('userscript', self.iFrame) addApiView('userscript.add_via_url', self.getViaUrl) + addApiView('userscript.bookmark', self.bookmark) addEvent('userscript.get_version', self.getVersion) - def getUserScript(self, filename = ''): + def bookmark(self): + + params = { + 'includes': fireEvent('userscript.get_includes', merge = True), + 'excludes': fireEvent('userscript.get_excludes', merge = True), + 'host': getParam('host', None), + } + + return self.renderTemplate(__file__, 'bookmark.js', **params) + + def getUserScript(self, random = '', filename = ''): params = { 'includes': fireEvent('userscript.get_includes', merge = True), @@ -42,14 +55,14 @@ class Userscript(Plugin): versions = fireEvent('userscript.get_provider_version') - version = 0 + version = self.version for v in versions: version += v return version def iFrame(self): - return index() + return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) def getViaUrl(self): diff --git a/couchpotato/core/plugins/userscript/static/userscript.js b/couchpotato/core/plugins/userscript/static/userscript.js index 63f5fc96..d6d5983c 100644 --- a/couchpotato/core/plugins/userscript/static/userscript.js +++ b/couchpotato/core/plugins/userscript/static/userscript.js @@ -62,34 +62,54 @@ var UserscriptSettingTab = new Class({ self.settings = App.getPage('Settings') self.settings.addEvent('create', function(){ - var tab = self.settings.createTab('userscript', { - 'label': 'Userscript', - 'name': 'userscript' + + // See if userscript can be installed + var userscript = false; + try { + if(Components.interfaces.gmIGreasemonkeyService) + userscript = true + } + catch(e){ + userscript = Browser.chrome === true; + } + + var host_url = window.location.protocol + '//' + window.location.host; + + self.settings.createGroup({ + 'name': 'userscript', + 'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''), + 'description': 'Easily add movies via imdb.com, appletrailers and more' + }).inject(self.settings.tabs.automation.content, 'top').adopt( + (userscript ? [new Element('a.userscript.button', { + 'text': 'Install userscript', + 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', + 'target': '_self' + }), new Element('span.or[text=or]')] : null), + new Element('span.bookmarklet').adopt( + new Element('a.button.green', { + 'text': '+CouchPotato', + 'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" + + host_url + Api.createUrl('userscript.bookmark') + + "?host="+ encodeURI(host_url + Api.createUrl('userscript.get')+randomString()+'/') + + "&r='+Math.random()*99999999);document.body.appendChild(e)})());", + 'target': '', + 'events': { + 'click': function(e){ + (e).stop() + alert('Drag it to your bookmark ;)') + } + } + }), + new Element('span', { + 'text': '⇽ Drag this to your bookmarks' + }) + ) + ).setStyles({ + 'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')" }); - self.tab = tab.tab; - self.content = tab.content; - - self.createUserscript(); - }); - }, - - createUserscript: function(){ - var self = this; - - - self.settings.createGroup({ - 'label': 'Install the Userscript' - }).inject(self.content).adopt( - new Element('a', { - 'text': 'Install userscript', - 'href': Api.createUrl('userscript.get')+'couchpotato.user.js', - 'target': '_self' - }) - ); - } }); @@ -106,7 +126,7 @@ window.addEvent('load', function(){ if(your_version && your_version < latest_version && checked_already < latest_version){ if(confirm("Update to the latest Userscript?\nYour version: " + your_version + ', new version: ' + latest_version )){ - document.location = Api.getOption('url')+'userscript.get/?couchpotato.user.js'; + document.location = Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js'; } Cookie.write(key, latest_version, {duration: 100}); } diff --git a/couchpotato/core/plugins/userscript/static/userscript.png b/couchpotato/core/plugins/userscript/static/userscript.png new file mode 100644 index 00000000..c8e76577 Binary files /dev/null and b/couchpotato/core/plugins/userscript/static/userscript.png differ diff --git a/couchpotato/core/plugins/userscript/template.js b/couchpotato/core/plugins/userscript/template.js index 64839fb7..d2d58adf 100644 --- a/couchpotato/core/plugins/userscript/template.js +++ b/couchpotato/core/plugins/userscript/template.js @@ -1,7 +1,7 @@ // ==UserScript== // @name CouchPotato UserScript // @description Add movies like a real CouchPotato -// @version {{version}} +// @version {{version}} // @match {{host}}* {% for include in includes %} @@ -57,7 +57,7 @@ if (typeof GM_addStyle == 'undefined'){ // Styles GM_addStyle('\ - #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius-topleft: 6px; -moz-border-radius-bottomleft: 6px; -webkit-border-top-left-radius: 6px; -webkit-border-bottom-left-radius: 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \ + #cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \ #cp_popup:hover { } \ #cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \ #cp_popup a#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \ @@ -72,8 +72,12 @@ var close_img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8 var osd = function(){ var navbar, newElement; + var createApiUrl = function(url){ + return host + api + "?url=" + escape(url) + }; + var iframe = create('iframe', { - 'src': host + api + "?url=" + escape(document.location.href), + 'src': createApiUrl(document.location.href), 'frameborder': 0, 'scrolling': 'no' }); @@ -81,32 +85,49 @@ var osd = function(){ var popup = create('div', { 'id': 'cp_popup' }); + + var onclick = function(){ + + // Try and get imdb url + try { + var regex = new RegExp(/tt(\d{7})/); + var imdb_id = document.body.innerHTML.match(regex)[0]; + if (imdb_id) + iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/')) + } + catch(e){} + + popup.innerHTML = ''; + popup.appendChild(create('a', { + 'innerHTML': '', + 'id': 'close_button', + 'onclick': function(){ + popup.innerHTML = ''; + popup.appendChild(add_button); + } + })); + popup.appendChild(iframe) + } + var add_button = create('a', { 'innerHTML': '', 'id': 'add_to', - 'onclick': function(){ - popup.innerHTML = ''; - popup.appendChild(create('a', { - 'innerHTML': '', - 'id': 'close_button', - 'onclick': function(){ - popup.innerHTML = ''; - popup.appendChild(add_button); - } - })); - popup.appendChild(iframe) - } + 'onclick': onclick }); popup.appendChild(add_button); document.body.parentNode.insertBefore(popup, document.body); + + // Auto fold open + if(document.body.getAttribute('cp_auto_open')) + onclick() }; var setVersion = function(){ - document.body.setAttribute('data-userscript_version', version) + document.body.setAttribute('data-userscript_version', version) }; if(document.location.href.indexOf(host) == -1) - osd(); + osd(); else - setVersion(); \ No newline at end of file + setVersion(); \ No newline at end of file diff --git a/couchpotato/core/plugins/wizard/static/wizard.css b/couchpotato/core/plugins/wizard/static/wizard.css index 8197b58a..d1aa99c8 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.css +++ b/couchpotato/core/plugins/wizard/static/wizard.css @@ -1,5 +1,3 @@ -/* @override http://127.0.0.1:5000/static/wizard/wizard.css */ - .page.wizard h1 { padding: 10px 30px; margin: 0; @@ -33,14 +31,14 @@ margin: 0; display: block; } - + .page.wizard .tabs li { display: inline-block; } .page.wizard .tabs li a { padding: 20px 30px; } - + .page.wizard .tab_wrapper .pointer { border-right: 10px solid transparent; border-left: 10px solid transparent; @@ -49,7 +47,7 @@ position: absolute; top: 60px; } - + .page.wizard .tab_content { margin: 20px 0 160px; } diff --git a/couchpotato/core/plugins/wizard/static/wizard.js b/couchpotato/core/plugins/wizard/static/wizard.js index 74d2b4db..e1e07a87 100644 --- a/couchpotato/core/plugins/wizard/static/wizard.js +++ b/couchpotato/core/plugins/wizard/static/wizard.js @@ -29,13 +29,13 @@ Page.Wizard = new Class({ }, 'finish': { 'title': 'Finish Up', - 'description': 'Are you done? Did you fill in everything or as much as possible? Yes, ok gogogo!', + 'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!', 'content': new Element('div').adopt( new Element('a.button.green', { 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!', 'events': { 'click': function(e){ - (e).stop(); + (e).preventDefault(); Api.request('settings.save', { 'data': { 'section': 'core', @@ -115,7 +115,14 @@ Page.Wizard = new Class({ if(tab_navigation && group_container){ tab_navigation.inject(tabs); // Tab navigation self.el.getElement('.tab_'+group).inject(group_container); // Tab content - if(self.headers[group]) tab_navigation.getElement('a').set('text', (self.headers[group].label || group).capitalize()); + if(self.headers[group]){ + var a = tab_navigation.getElement('a'); + a.set('text', (self.headers[group].label || group).capitalize()); + var url_split = a.get('href').split('wizard')[1].split('/'); + if(url_split.length > 3) + a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', '')); + + } } else { new Element('li.t_'+group).adopt( @@ -161,7 +168,7 @@ Page.Wizard = new Class({ if(nr == 0) func(); - + var ss = new ScrollSpy( { min: function(){ var c = g.getCoordinates(); diff --git a/couchpotato/core/providers/automation/cp/__init__.py b/couchpotato/core/providers/automation/cp/__init__.py index 914c1f53..a4b55a83 100644 --- a/couchpotato/core/providers/automation/cp/__init__.py +++ b/couchpotato/core/providers/automation/cp/__init__.py @@ -3,21 +3,4 @@ from .main import CP def start(): return CP() -config = [{ - 'name': 'cp', - 'groups': [ - { - 'tab': 'automation', - 'name': 'couchpotato_automation', - 'label': 'CouchPotato', - 'description': 'Enable automatic movie adding from CouchPotato', - 'options': [ - { - 'name': 'automation_enabled', - 'default': False, - 'type': 'enabler', - }, - ], - }, - ], -}] +config = [] diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index 148b2e7c..0d00179b 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -102,4 +102,4 @@ class YarrProvider(Provider): return [self.cat_backup_id] def found(self, new): - log.info('Found: score(%(score)s): %(name)s' % new) + log.info('Found: score(%(score)s) on %(provider)s: %(name)s' % new) diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 5d99ea9b..d18ecbf7 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin -import json import os import shutil import traceback @@ -31,11 +30,7 @@ class MetaDataBase(Plugin): root = self.getRootName(release) - try: - movie_info = json.loads(release['library'].get('info')) - except: - log.error('Failed to parse movie info: %s' % traceback.format_exc()) - movie_info = {} + movie_info = release['library'].get('info') for file_type in ['nfo', 'thumbnail', 'fanart']: try: diff --git a/couchpotato/core/providers/metadata/mediabrowser/__init__.py b/couchpotato/core/providers/metadata/mediabrowser/__init__.py index 35b84278..c061ce3c 100644 --- a/couchpotato/core/providers/metadata/mediabrowser/__init__.py +++ b/couchpotato/core/providers/metadata/mediabrowser/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'mediabrowser', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'mediabrowser_metadata', 'label': 'MediaBrowser', 'description': 'Enable metadata MediaBrowser can understand', diff --git a/couchpotato/core/providers/metadata/sonyps3/__init__.py b/couchpotato/core/providers/metadata/sonyps3/__init__.py index 88c6167f..002b8487 100644 --- a/couchpotato/core/providers/metadata/sonyps3/__init__.py +++ b/couchpotato/core/providers/metadata/sonyps3/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'sonyps3', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'sonyps3_metadata', 'label': 'Sony PS3', 'description': 'Enable metadata your Playstation 3 can understand', diff --git a/couchpotato/core/providers/metadata/wdtv/__init__.py b/couchpotato/core/providers/metadata/wdtv/__init__.py index edb9cc26..b3dab6e7 100644 --- a/couchpotato/core/providers/metadata/wdtv/__init__.py +++ b/couchpotato/core/providers/metadata/wdtv/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'wdtv', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'wdtv_metadata', 'label': 'WDTV', 'description': 'Enable metadata WDTV can understand', diff --git a/couchpotato/core/providers/metadata/xbmc/__init__.py b/couchpotato/core/providers/metadata/xbmc/__init__.py index d4ff12a6..2a9510e5 100644 --- a/couchpotato/core/providers/metadata/xbmc/__init__.py +++ b/couchpotato/core/providers/metadata/xbmc/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'xbmc', 'groups': [ { - 'tab': 'metadata', + 'tab': 'renamer', + 'subtab': 'metadata', 'name': 'xbmc_metadata', 'label': 'XBMC', 'description': 'Enable metadata XBMC can understand', diff --git a/couchpotato/core/providers/metadata/xbmc/main.py b/couchpotato/core/providers/metadata/xbmc/main.py index d1f0feed..2908c6f6 100644 --- a/couchpotato/core/providers/metadata/xbmc/main.py +++ b/couchpotato/core/providers/metadata/xbmc/main.py @@ -48,7 +48,7 @@ class XBMC(MetaDataBase): pass # Other values - types = ['rating', 'year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released'] + types = ['year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released'] for type in types: if ':' in type: @@ -73,7 +73,7 @@ class XBMC(MetaDataBase): votes.text = str(v) break except: - log.error('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc())) + log.debug('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc())) # Genre for genre in movie_info.get('genres', []): @@ -87,9 +87,9 @@ class XBMC(MetaDataBase): name.text = toUnicode(actor) # Directors - for director in movie_info.get('directors', []): + for director_name in movie_info.get('directors', []): director = SubElement(nfoxml, 'director') - director.text = toUnicode(director) + director.text = toUnicode(director_name) # Writers for writer in movie_info.get('writers', []): diff --git a/couchpotato/core/providers/movie/imdbapi/main.py b/couchpotato/core/providers/movie/imdbapi/main.py index f238e7c4..604b37c1 100644 --- a/couchpotato/core/providers/movie/imdbapi/main.py +++ b/couchpotato/core/providers/movie/imdbapi/main.py @@ -14,7 +14,7 @@ class IMDBAPI(MovieProvider): urls = { 'search': 'http://www.imdbapi.com/?%s', - 'info': 'http://www.imdbapi.com/?i=%s&tomatoes=true', + 'info': 'http://www.imdbapi.com/?i=%s', } http_time_between_calls = 0 @@ -32,8 +32,11 @@ class IMDBAPI(MovieProvider): if cached: result = self.parseMovie(cached) - log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') - return [result] + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') + return [result] + + return [] return [] @@ -44,8 +47,9 @@ class IMDBAPI(MovieProvider): if cached: result = self.parseMovie(cached) - log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') - return result + if result.get('titles') and len(result.get('titles')) > 0: + log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') + return result return {} @@ -57,11 +61,19 @@ class IMDBAPI(MovieProvider): if isinstance(movie, (str, unicode)): movie = json.loads(movie) + if movie.get('Response') == 'Parse Error': + return movie_data + + tmp_movie = movie.copy() + for key in tmp_movie: + if tmp_movie.get(key).lower() == 'n/a': + del movie[key] + movie_data = { - 'titles': [movie.get('Title', '')], + 'titles': [movie.get('Title')] if movie.get('Title') else [], 'original_title': movie.get('Title', ''), 'images': { - 'poster': [movie.get('Poster', '')], + 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], }, 'rating': { 'imdb': (tryFloat(movie.get('Rating', 0)), tryInt(movie.get('Votes', ''))), diff --git a/couchpotato/core/providers/movie/themoviedb/__init__.py b/couchpotato/core/providers/movie/themoviedb/__init__.py index 31441fc4..66ac536a 100644 --- a/couchpotato/core/providers/movie/themoviedb/__init__.py +++ b/couchpotato/core/providers/movie/themoviedb/__init__.py @@ -10,7 +10,7 @@ config = [{ 'tab': 'providers', 'name': 'tmdb', 'label': 'TheMovieDB', - 'advanced': True, + 'hidden': True, 'description': 'Used for all calls to TheMovieDB.', 'options': [ { diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py index 5f1595b7..2838c4ce 100644 --- a/couchpotato/core/providers/movie/themoviedb/main.py +++ b/couchpotato/core/providers/movie/themoviedb/main.py @@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider): def __init__(self): addEvent('movie.by_hash', self.byHash) - addEvent('movie.search', self.search) - addEvent('movie.info', self.getInfo) + addEvent('movie.search', self.search, priority = 1) + addEvent('movie.info', self.getInfo, priority = 1) addEvent('movie.info_by_tmdb', self.getInfoByTMDBId) # Use base wrapper @@ -131,7 +131,7 @@ class TheMovieDb(MovieProvider): # Images poster = self.getImage(movie, type = 'poster', size = 'cover') - backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280') + #backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280') poster_original = self.getImage(movie, type = 'poster', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') @@ -152,7 +152,7 @@ class TheMovieDb(MovieProvider): 'original_title': movie.get('original_name'), 'images': { 'poster': [poster] if poster else [], - 'backdrop': [backdrop] if backdrop else [], + #'backdrop': [backdrop] if backdrop else [], 'poster_original': [poster_original] if poster_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [], }, diff --git a/couchpotato/core/providers/nzb/moovee/__init__.py b/couchpotato/core/providers/nzb/moovee/__init__.py index 8d86be1f..f2f85d18 100644 --- a/couchpotato/core/providers/nzb/moovee/__init__.py +++ b/couchpotato/core/providers/nzb/moovee/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'moovee', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': '#alt.binaries.moovee', 'description': 'SD movies only', 'options': [ diff --git a/couchpotato/core/providers/nzb/mysterbin/__init__.py b/couchpotato/core/providers/nzb/mysterbin/__init__.py index 07be1d4e..0c759555 100644 --- a/couchpotato/core/providers/nzb/mysterbin/__init__.py +++ b/couchpotato/core/providers/nzb/mysterbin/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'mysterbin', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'Mysterbin', 'description': '', 'options': [ diff --git a/couchpotato/core/providers/nzb/newzbin/__init__.py b/couchpotato/core/providers/nzb/newzbin/__init__.py index ea0c27df..4ebd849d 100644 --- a/couchpotato/core/providers/nzb/newzbin/__init__.py +++ b/couchpotato/core/providers/nzb/newzbin/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'newzbin', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'newzbin', 'wizard': True, 'options': [ diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py index 81672d2d..96a34a3f 100644 --- a/couchpotato/core/providers/nzb/newzbin/main.py +++ b/couchpotato/core/providers/nzb/newzbin/main.py @@ -26,6 +26,7 @@ class Newzbin(NZBProvider, RSS): 1024: ['r5'], } cat_ids = [ + ([262144], ['bd50']), ([2097152], ['1080p']), ([524288], ['720p']), ([262144], ['brrip']), diff --git a/couchpotato/core/providers/nzb/newznab/__init__.py b/couchpotato/core/providers/nzb/newznab/__init__.py index f963e00d..212c9847 100644 --- a/couchpotato/core/providers/nzb/newznab/__init__.py +++ b/couchpotato/core/providers/nzb/newznab/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'newznab', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'newznab', 'description': 'Enable multiple NewzNab providers such as NZB.su', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/newznab/main.py b/couchpotato/core/providers/nzb/newznab/main.py index 9a7943e5..c17e6163 100644 --- a/couchpotato/core/providers/nzb/newznab/main.py +++ b/couchpotato/core/providers/nzb/newznab/main.py @@ -20,8 +20,10 @@ class Newznab(NZBProvider, RSS): } cat_ids = [ + ([2010], ['dvdr']), ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2040], ['720p', '1080p']), + ([2050], ['bd50']), ] cat_backup_id = 2000 diff --git a/couchpotato/core/providers/nzb/nzbclub/__init__.py b/couchpotato/core/providers/nzb/nzbclub/__init__.py index 18f4e33d..9c14e10f 100644 --- a/couchpotato/core/providers/nzb/nzbclub/__init__.py +++ b/couchpotato/core/providers/nzb/nzbclub/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbclub', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'NZBClub', 'description': '', 'options': [ diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index c6731eb5..07ae2c2d 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -59,9 +59,7 @@ class NZBClub(NZBProvider, RSS): size = enclosure['length'] date = self.getTextElement(nzb, "pubDate") - description = '' - if 'nfo files' in self.getTextElement(nzb, "description"): - description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000)) + description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000)) new = { 'id': nzbclub_id, @@ -71,12 +69,16 @@ class NZBClub(NZBProvider, RSS): 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'size': tryInt(size) / 1024 / 1024, 'url': enclosure['url'], - 'download': enclosure['url'], + 'download': enclosure['url'].replace(' ', '_'), 'detail_url': self.getTextElement(nzb, "link"), 'description': description, } new['score'] = fireEvent('score.calculate', new, movie, single = True) + if 'ARCHIVE inside ARCHIVE' in description: + log.info('Wrong: Seems to be passworded files: %s' % new['name']) + continue + is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality, imdb_results = False, single_category = False, single = True) diff --git a/couchpotato/core/providers/nzb/nzbindex/__init__.py b/couchpotato/core/providers/nzb/nzbindex/__init__.py index cf3139c9..8a3261bf 100644 --- a/couchpotato/core/providers/nzb/nzbindex/__init__.py +++ b/couchpotato/core/providers/nzb/nzbindex/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbindex', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbindex', 'description': 'Free provider, but less accurate.', 'options': [ diff --git a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py index 84d17074..82b6ef6e 100644 --- a/couchpotato/core/providers/nzb/nzbmatrix/__init__.py +++ b/couchpotato/core/providers/nzb/nzbmatrix/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbmatrix', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbmatrix', 'label': 'NZBMatrix', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/nzbs/__init__.py b/couchpotato/core/providers/nzb/nzbs/__init__.py index bd9f9a37..2ca89171 100644 --- a/couchpotato/core/providers/nzb/nzbs/__init__.py +++ b/couchpotato/core/providers/nzb/nzbs/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'nzbs', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'nzbs', 'description': 'Id and Key can be found on your nzbs.org RSS page.', 'wizard': True, diff --git a/couchpotato/core/providers/nzb/x264/__init__.py b/couchpotato/core/providers/nzb/x264/__init__.py index ef0e2f29..152be009 100644 --- a/couchpotato/core/providers/nzb/x264/__init__.py +++ b/couchpotato/core/providers/nzb/x264/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'x264', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': '#alt.binaries.hdtv.x264', 'description': 'HD movies only', 'options': [ diff --git a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py index e88ac81c..6514643e 100644 --- a/couchpotato/core/providers/torrent/kickasstorrents/__init__.py +++ b/couchpotato/core/providers/torrent/kickasstorrents/__init__.py @@ -7,7 +7,8 @@ config = [{ 'name': 'kickasstorrents', 'groups': [ { - 'tab': 'providers', + 'tab': 'searcher', + 'subtab': 'providers', 'name': 'KickAssTorrents', 'options': [ { diff --git a/couchpotato/core/providers/userscript/allocine/main.py b/couchpotato/core/providers/userscript/allocine/main.py index 91b44d95..8213ac2f 100644 --- a/couchpotato/core/providers/userscript/allocine/main.py +++ b/couchpotato/core/providers/userscript/allocine/main.py @@ -11,7 +11,7 @@ class AlloCine(UserscriptBase): return 'Url isn\'t from a movie' try: - data = self.urlopen(url) + data = self.getUrl(url) except: return diff --git a/couchpotato/core/providers/userscript/appletrailers/main.py b/couchpotato/core/providers/userscript/appletrailers/main.py index d7ce8ab3..693065d1 100644 --- a/couchpotato/core/providers/userscript/appletrailers/main.py +++ b/couchpotato/core/providers/userscript/appletrailers/main.py @@ -9,7 +9,7 @@ class AppleTrailers(UserscriptBase): def getMovie(self, url): try: - data = self.urlopen(url) + data = self.getUrl(url) except: return diff --git a/couchpotato/core/providers/userscript/base.py b/couchpotato/core/providers/userscript/base.py index d4b3b9f3..571b76c0 100644 --- a/couchpotato/core/providers/userscript/base.py +++ b/couchpotato/core/providers/userscript/base.py @@ -1,5 +1,6 @@ from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.variable import getImdb +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.helpers.variable import getImdb, md5 from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin from urlparse import urlparse @@ -42,9 +43,12 @@ class UserscriptBase(Plugin): return + def getUrl(self, url): + return self.getCache(md5(simplifyString(url)), url = url) + def getMovie(self, url): try: - data = self.urlopen(url) + data = self.getUrl(url) except: data = '' return self.getInfo(getImdb(data)) diff --git a/couchpotato/core/providers/userscript/letterboxd/__init__.py b/couchpotato/core/providers/userscript/letterboxd/__init__.py new file mode 100644 index 00000000..c8c17977 --- /dev/null +++ b/couchpotato/core/providers/userscript/letterboxd/__init__.py @@ -0,0 +1,6 @@ +from .main import Letterboxd + +def start(): + return Letterboxd() + +config = [] diff --git a/couchpotato/core/providers/userscript/letterboxd/main.py b/couchpotato/core/providers/userscript/letterboxd/main.py new file mode 100644 index 00000000..c0d91d79 --- /dev/null +++ b/couchpotato/core/providers/userscript/letterboxd/main.py @@ -0,0 +1,6 @@ +from couchpotato.core.providers.userscript.base import UserscriptBase + + +class Letterboxd(UserscriptBase): + + includes = ['*://letterboxd.com/film/*'] diff --git a/couchpotato/core/providers/userscript/rottentomatoes/__init__.py b/couchpotato/core/providers/userscript/rottentomatoes/__init__.py new file mode 100644 index 00000000..ee8266eb --- /dev/null +++ b/couchpotato/core/providers/userscript/rottentomatoes/__init__.py @@ -0,0 +1,6 @@ +from .main import RottenTomatoes + +def start(): + return RottenTomatoes() + +config = [] diff --git a/couchpotato/core/providers/userscript/rottentomatoes/main.py b/couchpotato/core/providers/userscript/rottentomatoes/main.py new file mode 100644 index 00000000..1d685903 --- /dev/null +++ b/couchpotato/core/providers/userscript/rottentomatoes/main.py @@ -0,0 +1,19 @@ +from BeautifulSoup import BeautifulSoup +from couchpotato.core.event import fireEvent +from couchpotato.core.providers.userscript.base import UserscriptBase + +class RottenTomatoes(UserscriptBase): + + includes = ['*://www.rottentomatoes.com/m/*'] + + def getMovie(self, url): + + try: + data = self.getUrl(url) + except: + return + + html = BeautifulSoup(data) + title = html.find('span', {'itemprop':'name'}).text + info = fireEvent('scanner.name_year', title, single = True) + return self.search(info['name'], info['year']) diff --git a/couchpotato/core/providers/userscript/tmdb/main.py b/couchpotato/core/providers/userscript/tmdb/main.py index d58d8197..6205851e 100644 --- a/couchpotato/core/providers/userscript/tmdb/main.py +++ b/couchpotato/core/providers/userscript/tmdb/main.py @@ -10,4 +10,7 @@ class TMDB(UserscriptBase): def getMovie(self, url): match = re.search('(?P\d+)', url) movie = fireEvent('movie.info_by_tmdb', id = match.group('id'), merge = True) - return self.getInfo(movie['imdb']) + + if movie['imdb']: + return self.getInfo(movie['imdb']) + diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 6bed3dd6..60c77cb2 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -16,8 +16,41 @@ class Settings(object): def __init__(self): - addApiView('settings', self.view) - addApiView('settings.save', self.saveView) + addApiView('settings', self.view, docs = { + 'desc': 'Return the options and its values of settings.conf. Including the default values and group ordering used on the settings page.', + 'return': {'type': 'object', 'example': """{ + // objects like in __init__.py of plugin + "options": { + "moovee" : { + "groups" : [{ + "description" : "SD movies only", + "name" : "#alt.binaries.moovee", + "options" : [{ + "default" : false, + "name" : "enabled", + "type" : "enabler" + }], + "tab" : "providers" + }], + "name" : "moovee" + } + }, + // object structured like settings.conf + "values": { + "moovee": { + "enabled": false + } + } +}"""} + }) + addApiView('settings.save', self.saveView, docs = { + 'desc': 'Save setting to config file (settings.conf)', + 'params': { + 'section': {'desc': 'The section name in settings.conf'}, + 'option': {'desc': 'The option name'}, + 'value': {'desc': 'The value you want to save'}, + } + }) def setFile(self, config_file): self.file = config_file @@ -128,7 +161,6 @@ class Settings(object): if not self.options.get(section_name): self.options[section_name] = options else: - options['groups'] = self.options[section_name].get('groups') + options.get('groups') self.options[section_name] = mergeDicts(self.options[section_name], options) def getOptions(self): @@ -136,7 +168,6 @@ class Settings(object): def view(self): - return jsonified({ 'options': self.getOptions(), 'values': self.getValues() diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 69aa3f17..4dd54d32 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -1,10 +1,12 @@ +from couchpotato.core.helpers.encoding import toUnicode from elixir.entity import Entity from elixir.fields import Field from elixir.options import options_defaults, using_options from elixir.relationships import ManyToMany, OneToMany, ManyToOne -from libs.elixir.relationships import OneToOne from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \ - String + String, TypeDecorator +import json +import time options_defaults["shortnames"] = True @@ -15,13 +17,29 @@ options_defaults["shortnames"] = True # http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata __session__ = None +class SetEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, set): + return list(obj) + return json.JSONEncoder.default(self, obj) + + +class JsonType(TypeDecorator): + impl = UnicodeText + + def process_bind_param(self, value, dialect): + return toUnicode(json.dumps(value, cls = SetEncoder)) + + def process_result_value(self, value, dialect): + return json.loads(value if value else '{}') + class Movie(Entity): """Movie Resource a movie could have multiple releases The files belonging to the movie object are global for the whole movie such as trailers, nfo, thumbnails""" - last_edit = Field(Integer) + last_edit = Field(Integer, default = lambda: int(time.time())) library = ManyToOne('Library') status = ManyToOne('Status') @@ -34,12 +52,11 @@ class Library(Entity): """""" year = Field(Integer) - identifier = Field(String(20)) - rating = Field(Float) + identifier = Field(String(20), index = True) plot = Field(UnicodeText) tagline = Field(UnicodeText(255)) - info = Field(UnicodeText) + info = Field(JsonType) status = ManyToOne('Status') movies = OneToMany('Movie') @@ -52,8 +69,8 @@ class LibraryTitle(Entity): using_options(order_by = '-default') title = Field(Unicode) - simple_title = Field(Unicode) - default = Field(Boolean) + simple_title = Field(Unicode, index = True) + default = Field(Boolean, index = True) language = OneToMany('Language') libraries = ManyToOne('Library') @@ -62,7 +79,7 @@ class LibraryTitle(Entity): class Language(Entity): """""" - identifier = Field(String(20)) + identifier = Field(String(20), index = True) label = Field(Unicode) titles = ManyToOne('LibraryTitle') @@ -72,7 +89,7 @@ class Release(Entity): """Logically groups all files that belong to a certain release, such as parts of a movie, subtitles.""" - identifier = Field(String(100)) + identifier = Field(String(100), index = True) movie = ManyToOne('Movie') status = ManyToOne('Status') @@ -85,7 +102,7 @@ class Release(Entity): class ReleaseInfo(Entity): """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(50)) + identifier = Field(String(50), index = True) value = Field(Unicode(255), nullable = False) release = ManyToOne('Release') @@ -107,7 +124,7 @@ class Quality(Entity): identifier = Field(String(20), unique = True) label = Field(Unicode(20)) - order = Field(Integer) + order = Field(Integer, index = True) size_min = Field(Integer) size_max = Field(Integer) @@ -121,7 +138,7 @@ class Profile(Entity): using_options(order_by = 'order') label = Field(Unicode(50)) - order = Field(Integer) + order = Field(Integer, index = True) core = Field(Boolean) hide = Field(Boolean) @@ -133,7 +150,7 @@ class ProfileType(Entity): """""" using_options(order_by = 'order') - order = Field(Integer) + order = Field(Integer, index = True) finish = Field(Boolean) wait_for = Field(Integer) @@ -170,7 +187,7 @@ class FileType(Entity): class FileProperty(Entity): """Properties that can be bound to a file for off-line usage""" - identifier = Field(String(20)) + identifier = Field(String(20), index = True) value = Field(Unicode(255), nullable = False) file = ManyToOne('File') @@ -180,8 +197,8 @@ class History(Entity): """History of actions that are connected to a certain release, such as, renamed to, downloaded, deleted, download subtitles etc""" - added = Field(Integer) - message = Field(UnicodeText()) + added = Field(Integer, default = lambda: int(time.time())) + message = Field(UnicodeText) type = Field(Unicode(50)) release = ManyToOne('Release') @@ -196,6 +213,15 @@ class RenameHistory(Entity): file = ManyToOne('File') +class Notification(Entity): + using_options(order_by = 'added') + + added = Field(Integer, default = lambda: int(time.time())) + read = Field(Boolean, default = False) + message = Field(Unicode(255)) + data = Field(JsonType) + + class Folder(Entity): """Renamer destination folders.""" diff --git a/couchpotato/environment.py b/couchpotato/environment.py index a4d50747..ac256c18 100644 --- a/couchpotato/environment.py +++ b/couchpotato/environment.py @@ -1,6 +1,7 @@ from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.loader import Loader from couchpotato.core.settings import Settings +import os class Env(object): @@ -61,8 +62,12 @@ class Env(object): return s @staticmethod - def getPermission(type): - return int(Env.get('settings').get('permission_%s' % type, default = 0777)) + def getPermission(setting_type): + perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777') + if perm[0] == '0': + return int(perm, 8) + else: + return int(perm) @staticmethod def fireEvent(*args, **kwargs): @@ -71,3 +76,14 @@ class Env(object): @staticmethod def addEvent(*args, **kwargs): return addEvent(*args, **kwargs) + + @staticmethod + def getPid(): + try: + try: + parent = os.getppid() + except: + parent = None + return '%d %s' % (os.getpid(), '(%d)' % parent if parent and parent > 1 else '') + except: + return 0 diff --git a/couchpotato/runner.py b/couchpotato/runner.py index 6b8196ea..217f52ba 100644 --- a/couchpotato/runner.py +++ b/couchpotato/runner.py @@ -128,24 +128,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En # Load migrations - from migrate.versioning.api import version_control, db_version, version, upgrade - db = Env.get('db_path') - repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') - logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration - - latest_db_version = version(repo) - initialize = True - try: - current_db_version = db_version(db, repo) + db = Env.get('db_path') + if os.path.isfile(db.replace('sqlite:///', '')): initialize = False - except: - version_control(db, repo, version = latest_db_version) - current_db_version = db_version(db, repo) - if current_db_version < latest_db_version and not debug: - log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) - upgrade(db, repo) + from migrate.versioning.api import version_control, db_version, version, upgrade + repo = os.path.join(base_path, 'couchpotato', 'core', 'migration') + logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration + + latest_db_version = version(repo) + try: + current_db_version = db_version(db, repo) + except: + version_control(db, repo, version = latest_db_version) + current_db_version = db_version(db, repo) + + if current_db_version < latest_db_version and not debug: + log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) + upgrade(db, repo) # Configure Database from couchpotato.core.settings.model import setup @@ -185,4 +186,26 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En if fire_load: fireEventAsync('app.load') # Go go go! - app.run(**config) + try_restart = True + restart_tries = 5 + while try_restart: + try: + app.run(**config) + except Exception, e: + try: + nr, msg = e + if nr == 48: + log.info('Already in use, try %s more time after few seconds' % restart_tries) + time.sleep(1) + restart_tries -= 1 + + if restart_tries > 0: + continue + else: + return + except: + pass + + raise + + try_restart = False diff --git a/couchpotato/static/images/checks.png b/couchpotato/static/images/checks.png deleted file mode 100644 index 3561d917..00000000 Binary files a/couchpotato/static/images/checks.png and /dev/null differ diff --git a/couchpotato/static/images/icon.undo.png b/couchpotato/static/images/icon.undo.png new file mode 100644 index 00000000..07f907dc Binary files /dev/null and b/couchpotato/static/images/icon.undo.png differ diff --git a/couchpotato/static/images/spinner.gif b/couchpotato/static/images/spinner.gif deleted file mode 100644 index d0bce154..00000000 Binary files a/couchpotato/static/images/spinner.gif and /dev/null differ diff --git a/couchpotato/static/images/sprite.png b/couchpotato/static/images/sprite.png new file mode 100644 index 00000000..6af04a7e Binary files /dev/null and b/couchpotato/static/images/sprite.png differ diff --git a/couchpotato/static/scripts/api.js b/couchpotato/static/scripts/api.js index f39ec6f2..f14eb14d 100644 --- a/couchpotato/static/scripts/api.js +++ b/couchpotato/static/scripts/api.js @@ -11,7 +11,7 @@ var ApiClass = new Class({ var r_type = self.options.is_remote ? 'JSONP' : 'JSON'; return new Request[r_type](Object.merge({ - 'callbackKey': 'json_callback', + 'callbackKey': 'callback_func', 'method': 'get', 'url': self.createUrl(type), }, options)).send() diff --git a/couchpotato/static/scripts/block.js b/couchpotato/static/scripts/block.js index da816d24..82193ca5 100644 --- a/couchpotato/static/scripts/block.js +++ b/couchpotato/static/scripts/block.js @@ -7,13 +7,13 @@ var BlockBase = new Class({ initialize: function(parent, options){ var self = this; self.setOptions(options); - + self.page = parent; self.create(); }, - + create: function(){ this.el = new Element('div.block'); }, @@ -21,11 +21,11 @@ var BlockBase = new Class({ getParent: function(){ return this.page }, - + hide: function(){ this.el.hide(); }, - + show: function(){ this.el.show(); }, diff --git a/couchpotato/static/scripts/block/menu.js b/couchpotato/static/scripts/block/menu.js new file mode 100644 index 00000000..4dc143d4 --- /dev/null +++ b/couchpotato/static/scripts/block/menu.js @@ -0,0 +1,45 @@ +Block.Menu = new Class({ + + Extends: BlockBase, + + options: { + 'class': 'menu' + }, + + create: function(){ + var self = this; + + self.el = new Element('div', { + 'class': 'more_menu '+self.options['class'] + }).adopt( + self.wrapper = new Element('div.wrapper').adopt( + self.more_option_ul = new Element('ul') + ), + new Element('a.button.onlay', { + 'events': { + 'click': function(){ + self.el.toggleClass('show') + self.fireEvent(self.el.hasClass('show') ? 'open' : 'close') + + if(self.el.hasClass('show')) + this.addEvent('outerClick', function(){ + self.el.removeClass('show') + this.removeEvents('outerClick'); + }) + else + this.removeEvents('outerClick'); + + } + } + }) + ) + + }, + + addLink: function(tab, position){ + var self = this; + var el = new Element('li').adopt(tab).inject(self.more_option_ul, position || 'bottom'); + return el; + } + +}); \ No newline at end of file diff --git a/couchpotato/static/scripts/block/navigation.js b/couchpotato/static/scripts/block/navigation.js index b0674b25..b6886f8d 100644 --- a/couchpotato/static/scripts/block/navigation.js +++ b/couchpotato/static/scripts/block/navigation.js @@ -35,7 +35,7 @@ Block.Navigation = new Class({ addTab: function(tab){ var self = this - return new Element('li').adopt( + return new Element('li.tab_'+(tab.text.toLowerCase() || 'unknown')).adopt( new Element('a', tab) ).inject(self.nav) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index 8fbd00cb..edafdf4c 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -43,7 +43,7 @@ var CouchPotato = new Class({ pushState: function(e){ var self = this; if((!e.meta && Browser.Platform.mac) || (!e.control && !Browser.Platform.mac)){ - (e).stop(); + (e).preventDefault(); var url = e.target.get('href'); if(History.getPath() != url) History.push(url); @@ -59,13 +59,36 @@ var CouchPotato = new Class({ $(self.block.header).addClass('header').adopt( new Element('div').adopt( self.block.navigation = new Block.Navigation(self, {}), - self.block.search = new Block.Search(self, {}) + self.block.search = new Block.Search(self, {}), + self.block.more = new Block.Menu(self, {}) ) ), self.content = new Element('div.content'), self.block.footer = new Block.Footer(self, {}) ); - + + [new Element('a.orange', { + 'text': 'Restart', + 'events': { + 'click': self.restart.bind(self) + } + }), + new Element('a.red', { + 'text': 'Shutdown', + 'events': { + 'click': self.shutdown.bind(self) + } + }), + new Element('a', { + 'text': 'Check for updates', + 'events': { + 'click': self.checkForUpdate.bind(self) + } + })].each(function(a){ + self.block.more.addLink(a) + }) + + new ScrollSpy({ min: 10, onLeave: function(){ @@ -108,7 +131,7 @@ var CouchPotato = new Class({ try { var page = self.pages[page_name] || self.pages.Wanted; - page.open(action, params); + page.open(action, params, current_url); page.show(); } catch(e){ @@ -138,19 +161,28 @@ var CouchPotato = new Class({ self.checkAvailable(1000); }, - restart: function(){ + restart: function(message, title){ var self = this; - self.blockPage('Restarting... please wait. If this takes to long, something must have gone wrong.'); + self.blockPage(message || 'Restarting... please wait. If this takes to long, something must have gone wrong.', title); Api.request('app.restart'); self.checkAvailable(1000); }, + checkForUpdate: function(func){ + var self = this; + + Updater.check(func) + + self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); + self.checkAvailable(3000); + }, + checkAvailable: function(delay){ var self = this; (function(){ - + Api.request('app.available', { 'onFailure': function(){ self.checkAvailable.delay(1000, self); @@ -161,32 +193,42 @@ var CouchPotato = new Class({ self.fireEvent('load'); } }); - + }).delay(delay || 0) }, blockPage: function(message, title){ var self = this; - if(!self.mask){ - var body = $(document.body); - self.mask = new Spinner(document.body, { - 'message': new Element('div').adopt( - new Element('h1', {'text': title || 'Unavailable'}), - new Element('div', {'text': message || 'Something must have crashed.. check the logs ;)'}) - ) - }); - } - self.mask.show(); + var body = $(document.body); + self.mask = new Element('div.mask').adopt( + new Element('div').adopt( + new Element('h1', {'text': title || 'Unavailable'}), + new Element('div', {'text': message || 'Something must have crashed.. check the logs ;)'}) + ) + ).fade('hide').inject(document.body).fade('in'); + + createSpinner(self.mask, { + 'top': -50 + }); }, unBlockPage: function(){ var self = this; - self.mask.hide(); + self.mask.get('tween').start('opacity', 0).chain(function(){ + this.element.destroy() + }); }, createUrl: function(action, params){ return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '') + }, + + notify: function(options){ + return this.growl.notify({ + title: "this scrolls away", + text: "test - hello there. mouseover to pause away action" + }); } }); @@ -214,7 +256,7 @@ var Route = new Class({ self.page = (url.length > 0) ? url.shift() : self.defaults.page self.action = (url.length > 0) ? url.shift() : self.defaults.action - self.params = self.defaults.params + self.params = Object.merge({}, self.defaults.params); if(url.length > 1){ var key url.each(function(el, nr){ @@ -226,6 +268,9 @@ var Route = new Class({ } }) } + else if(url.length == 1){ + self.params[url] = true; + } return self }, @@ -343,3 +388,22 @@ function randomString(length, extra) { })(); +var createSpinner = function(target, options){ + var opts = Object.merge({ + lines: 12, + length: 5, + width: 4, + radius: 9, + color: '#fff', + speed: 1.9, + trail: 53, + shadow: false, + hwaccel: true, + className: 'spinner', + zIndex: 2e9, + top: 'auto', + left: 'auto' + }, options); + + return new Spinner(opts).spin(target); +} \ No newline at end of file diff --git a/couchpotato/static/scripts/library/mootools.js b/couchpotato/static/scripts/library/mootools.js index a4d83f8d..9917ad32 100644 --- a/couchpotato/static/scripts/library/mootools.js +++ b/couchpotato/static/scripts/library/mootools.js @@ -8,6 +8,9 @@ web build: packager build: - packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Delegation Core/Element.Dimensions Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request.JSON Core/Cookie Core/DOMReady +... +*/ + /* --- @@ -17,7 +20,7 @@ description: The heart of MooTools. license: MIT-style license. -copyright: Copyright (c) 2006-2010 [Valerio Proietti](http://mad4milk.net/). +copyright: Copyright (c) 2006-2012 [Valerio Proietti](http://mad4milk.net/). authors: The MooTools production team (http://mootools.net/developers/) @@ -33,8 +36,8 @@ provides: [Core, MooTools, Type, typeOf, instanceOf, Native] (function(){ this.MooTools = { - version: '1.4.2', - build: '552dfd4704fccffed444e0211c50831a2bfe209f' + version: '1.4.5', + build: 'ab8ea8824dc3b24b6666867a2c4ed58ebb762cf0' }; // typeOf, instanceOf @@ -61,6 +64,9 @@ var instanceOf = this.instanceOf = function(item, object){ if (constructor === object) return true; constructor = constructor.parent; } + /**/ + if (!item.hasOwnProperty) return false; + /**/ return item instanceof object; }; @@ -93,8 +99,9 @@ Function.prototype.overloadGetter = function(usePlural){ var self = this; return function(a){ var args, result; - if (usePlural || typeof a != 'string') args = a; + if (typeof a != 'string') args = a; else if (arguments.length > 1) args = arguments; + else if (usePlural) args = [a]; if (args){ result = {}; for (var i = 0; i < args.length; i++) result[args[i]] = self.call(this, args[i]); @@ -251,14 +258,18 @@ var force = function(name, object, methods){ proto = prototype[key]; if (generic) generic.protect(); - - if (isType && proto){ - delete prototype[key]; - prototype[key] = proto.protect(); - } + if (isType && proto) object.implement(key, proto.protect()); } - if (isType) object.implement(prototype); + if (isType){ + var methodsEnumerable = prototype.propertyIsEnumerable(methods[0]); + object.forEachMethod = function(fn){ + if (!methodsEnumerable) for (var i = 0, l = methods.length; i < l; i++){ + fn.call(prototype, prototype[methods[i]], methods[i]); + } + for (var key in prototype) fn.call(prototype, prototype[key], key) + }; + } return force; }; @@ -429,8 +440,9 @@ Array.implement({ filter: function(fn, bind){ var results = []; - for (var i = 0, l = this.length >>> 0; i < l; i++){ - if ((i in this) && fn.call(bind, this[i], i, this)) results.push(this[i]); + for (var value, i = 0, l = this.length >>> 0; i < l; i++) if (i in this){ + value = this[i]; + if (fn.call(bind, value, i, this)) results.push(value); } return results; }, @@ -1787,8 +1799,14 @@ local.setDocument = function(document){ // contains // FIXME: Add specs: local.contains should be different for xml and html documents? - features.contains = (root && this.isNativeCode(root.contains)) ? function(context, node){ + var nativeRootContains = root && this.isNativeCode(root.contains), + nativeDocumentContains = document && this.isNativeCode(document.contains); + + features.contains = (nativeRootContains && nativeDocumentContains) ? function(context, node){ return context.contains(node); + } : (nativeRootContains && !nativeDocumentContains) ? function(context, node){ + // IE8 does not have .contains on document. + return context === node || ((context === document) ? document.documentElement : context).contains(node); } : (root && root.compareDocumentPosition) ? function(context, node){ return context === node || !!(context.compareDocumentPosition(node) & 16); } : function(context, node){ @@ -2183,7 +2201,7 @@ local.matchSelector = function(node, tag, id, classes, attributes, pseudos){ var i, part, cls; if (classes) for (i = classes.length; i--;){ - cls = node.getAttribute('class') || node.className; + cls = this.getAttribute(node, 'class'); if (!(cls && classes[i].regexp.test(cls))) return false; } if (attributes) for (i = attributes.length; i--;){ @@ -2369,7 +2387,7 @@ var pseudos = { 'nth-last-of-type': local.createNTHPseudo('lastChild', 'previousSibling', 'posNTHTypeLast', true), 'index': function(node, index){ - return this['pseudo:nth-child'](node, '' + index + 1); + return this['pseudo:nth-child'](node, '' + (index + 1)); }, 'even': function(node){ @@ -2441,10 +2459,6 @@ for (var p in pseudos) local['pseudo:' + p] = pseudos[p]; var attributeGetters = local.attributeGetters = { - 'class': function(){ - return this.getAttribute('class') || this.className; - }, - 'for': function(){ return ('htmlFor' in this) ? this.htmlFor : this.getAttribute('for'); }, @@ -2479,7 +2493,7 @@ attributeGetters.MAXLENGTH = attributeGetters.maxLength = attributeGetters.maxle var Slick = local.Slick = (this.Slick || {}); -Slick.version = '1.1.6'; +Slick.version = '1.1.7'; // Slick finder @@ -2638,7 +2652,10 @@ new Type('Element', Element).mirror(function(name){ if (!Browser.Element){ Element.parent = Object; - Element.Prototype = {'$family': Function.from('element').hide()}; + Element.Prototype = { + '$constructor': Element, + '$family': Function.from('element').hide() + }; Element.mirror(function(name, method){ Element.Prototype[name] = method; @@ -2753,16 +2770,17 @@ if (object[1] == 1) Elements.implement('splice', function(){ return result; }.protect()); -Elements.implement(Array.prototype); +Array.forEachMethod(function(method, name){ + Elements.implement(name, method); +}); Array.mirror(Elements); /**/ var createElementAcceptsHTML; try { - var x = document.createElement(''); - createElementAcceptsHTML = (x.name == 'x'); -} catch(e){} + createElementAcceptsHTML = (document.createElement('').name == 'x'); +} catch (e){} var escapeQuotes = function(html){ return ('' + html).replace(/&/g, '&').replace(/"/g, '"'); @@ -2821,7 +2839,11 @@ Document.implement({ element: function(el, nocash){ Slick.uidOf(el); if (!nocash && !el.$family && !(/^(?:object|embed)$/i).test(el.tagName)){ - el._fireEvent = el.fireEvent; + var fireEvent = el.fireEvent; + // wrapping needed in IE7, or else crash + el._fireEvent = function(type, event){ + return fireEvent(type, event); + }; Object.append(el, Element.Prototype); } return el; @@ -3001,13 +3023,8 @@ Array.forEach([ properties[property.toLowerCase()] = property; }); -Object.append(properties, { - 'html': 'innerHTML', - 'text': (function(){ - var temp = document.createElement('div'); - return (temp.textContent == null) ? 'innerText': 'textContent'; - })() -}); +properties.html = 'innerHTML'; +properties.text = (document.createElement('div').textContent == null) ? 'innerText': 'textContent'; Object.forEach(properties, function(real, key){ propertySetters[key] = function(node, value){ @@ -3056,7 +3073,7 @@ Object.append(propertySetters, { }, 'value': function(node, value){ - node.value = value || ''; + node.value = (value != null) ? value : ''; } }); @@ -3072,10 +3089,31 @@ try { el.type = 'button'; } catch(e){} if (el.type != 'button') propertySetters.type = function(node, value){ node.setAttribute('type', value); }; +el = null; /* */ +/**/ +var input = document.createElement('input'); +input.value = 't'; +input.type = 'submit'; +if (input.value != 't') propertySetters.type = function(node, type){ + var value = node.value; + node.type = type; + node.value = value; +}; +input = null; +/**/ + /* getProperty, setProperty */ +/* */ +var pollutesGetAttribute = (function(div){ + div.random = 'attribute'; + return (div.getAttribute('random') == 'attribute'); +})(document.createElement('div')); + +/* */ + Element.implement({ setProperty: function(name, value){ @@ -3083,8 +3121,21 @@ Element.implement({ if (setter){ setter(this, value); } else { - if (value == null) this.removeAttribute(name); - else this.setAttribute(name, value); + /* */ + if (pollutesGetAttribute) var attributeWhiteList = this.retrieve('$attributeWhiteList', {}); + /* */ + + if (value == null){ + this.removeAttribute(name); + /* */ + if (pollutesGetAttribute) delete attributeWhiteList[name]; + /* */ + } else { + this.setAttribute(name, '' + value); + /* */ + if (pollutesGetAttribute) attributeWhiteList[name] = true; + /* */ + } } return this; }, @@ -3097,6 +3148,18 @@ Element.implement({ getProperty: function(name){ var getter = propertyGetters[name.toLowerCase()]; if (getter) return getter(this); + /* */ + if (pollutesGetAttribute){ + var attr = this.getAttributeNode(name), attributeWhiteList = this.retrieve('$attributeWhiteList', {}); + if (!attr) return null; + if (attr.expando && !attributeWhiteList[name]){ + var outer = this.outerHTML; + // segment by the opening tag and find mention of attribute name + if (outer.substr(0, outer.search(/\/?['"]?>(?![^<]*<['"])/)).indexOf(name) < 0) return null; + attributeWhiteList[name] = true; + } + } + /* */ var result = Slick.getAttribute(this, name); return (!result && !Slick.hasAttribute(this, name)) ? null : result; }, @@ -3223,7 +3286,7 @@ var get = function(uid){ }; var clean = function(item){ - var uid = item.uid; + var uid = item.uniqueNumber; if (item.removeEvents) item.removeEvents(); if (item.clearAttributes) item.clearAttributes(); if (uid != null){ @@ -3269,7 +3332,7 @@ Element.implement({ if (node.clearAttributes){ node.clearAttributes(); node.mergeAttributes(element); - node.removeAttribute('uid'); + node.removeAttribute('uniqueNumber'); if (node.options){ var no = node.options, eo = element.options; for (var j = no.length; j--;) no[j].selected = eo[j].selected; @@ -3369,60 +3432,77 @@ Element.Properties.tag = { }; -/**/ -Element.Properties.html = (function(){ +Element.Properties.html = { - var tableTest = Function.attempt(function(){ - var table = document.createElement('table'); - table.innerHTML = ''; - }); + set: function(html){ + if (html == null) html = ''; + else if (typeOf(html) == 'array') html = html.join(''); + this.innerHTML = html; + }, - var wrapper = document.createElement('div'); - - var translations = { - table: [1, '', '
'], - select: [1, ''], - tbody: [2, '', '
'], - tr: [3, '', '
'] - }; - translations.thead = translations.tfoot = translations.tbody; - - /**/ - // technique by jdbarlett - http://jdbartlett.com/innershiv/ - wrapper.innerHTML = ''; - var HTML5Test = wrapper.childNodes.length == 1; - if (!HTML5Test){ - var tags = 'abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video'.split(' '), - fragment = document.createDocumentFragment(), l = tags.length; - while (l--) fragment.createElement(tags[l]); - fragment.appendChild(wrapper); + erase: function(){ + this.innerHTML = ''; } - /**/ - var html = { - set: function(html){ - if (typeOf(html) == 'array') html = html.join(''); +}; - var wrap = (!tableTest && translations[this.get('tag')]); - /**/ - if (!wrap && !HTML5Test) wrap = [0, '', '']; - /**/ - if (wrap){ - var first = wrapper; - first.innerHTML = wrap[1] + html + wrap[2]; - for (var i = wrap[0]; i--;) first = first.firstChild; - this.empty().adopt(first.childNodes); - } else { - this.innerHTML = html; - } - } - }; +/**/ +// technique by jdbarlett - http://jdbartlett.com/innershiv/ +var div = document.createElement('div'); +div.innerHTML = ''; +var supportsHTML5Elements = (div.childNodes.length == 1); +if (!supportsHTML5Elements){ + var tags = 'abbr article aside audio canvas datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video'.split(' '), + fragment = document.createDocumentFragment(), l = tags.length; + while (l--) fragment.createElement(tags[l]); +} +div = null; +/**/ - html.erase = html.set; +/**/ +var supportsTableInnerHTML = Function.attempt(function(){ + var table = document.createElement('table'); + table.innerHTML = ''; + return true; +}); - return html; -})(); -/**/ +/**/ +var tr = document.createElement('tr'), html = ''; +tr.innerHTML = html; +var supportsTRInnerHTML = (tr.innerHTML == html); +tr = null; +/**/ + +if (!supportsTableInnerHTML || !supportsTRInnerHTML || !supportsHTML5Elements){ + + Element.Properties.html.set = (function(set){ + + var translations = { + table: [1, '', '
'], + select: [1, ''], + tbody: [2, '', '
'], + tr: [3, '', '
'] + }; + + translations.thead = translations.tfoot = translations.tbody; + + return function(html){ + var wrap = translations[this.get('tag')]; + if (!wrap && !supportsHTML5Elements) wrap = [0, '', '']; + if (!wrap) return set.call(this, html); + + var level = wrap[0], wrapper = document.createElement('div'), target = wrapper; + if (!supportsHTML5Elements) fragment.appendChild(wrapper); + wrapper.innerHTML = [wrap[1], html, wrap[2]].flatten().join(''); + while (level--) target = target.firstChild; + this.empty().adopt(target.childNodes); + if (!supportsHTML5Elements) fragment.removeChild(wrapper); + wrapper = null; + }; + + })(Element.Properties.html.set); +} +/*
*/ /**/ var testForm = document.createElement('form'); @@ -3454,11 +3534,11 @@ if (testForm.firstChild.value != 's') Element.Properties.value = { } }; +testForm = null; /**/ /**/ -var el = document.createElement('div'); -if (el.getAttributeNode('id')) Element.Properties.id = { +if (document.createElement('div').getAttributeNode('id')) Element.Properties.id = { set: function(id){ this.id = this.getAttributeNode('id').value = id; }, @@ -3494,6 +3574,15 @@ provides: Element.Style var html = document.html; +// +// Check for oldIE, which does not remove styles when they're set to null +var el = document.createElement('div'); +el.style.color = 'red'; +el.style.color = null; +var doesNotRemoveStyles = el.style.color == 'red'; +el = null; +// + Element.Properties.styles = {set: function(styles){ this.setStyles(styles); }}; @@ -3504,17 +3593,19 @@ var hasOpacity = (html.style.opacity != null), var setVisibility = function(element, opacity){ element.store('$opacity', opacity); - element.style.visibility = opacity > 0 ? 'visible' : 'hidden'; + element.style.visibility = opacity > 0 || opacity == null ? 'visible' : 'hidden'; }; var setOpacity = (hasOpacity ? function(element, opacity){ element.style.opacity = opacity; } : (hasFilter ? function(element, opacity){ - if (!element.currentStyle || !element.currentStyle.hasLayout) element.style.zoom = 1; - opacity = (opacity * 100).limit(0, 100).round(); - opacity = (opacity == 100) ? '' : 'alpha(opacity=' + opacity + ')'; - var filter = element.style.filter || element.getComputedStyle('filter') || ''; - element.style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity; + var style = element.style; + if (!element.currentStyle || !element.currentStyle.hasLayout) style.zoom = 1; + if (opacity == null || opacity == 1) opacity = ''; + else opacity = 'alpha(opacity=' + (opacity * 100).limit(0, 100).round() + ')'; + var filter = style.filter || element.getComputedStyle('filter') || ''; + style.filter = reAlpha.test(filter) ? filter.replace(reAlpha, opacity) : filter + opacity; + if (!style.filter) style.removeAttribute('filter'); } : setVisibility)); var getOpacity = (hasOpacity ? function(element){ @@ -3544,7 +3635,8 @@ Element.implement({ setStyle: function(property, value){ if (property == 'opacity'){ - setOpacity(this, parseFloat(value)); + if (value != null) value = parseFloat(value); + setOpacity(this, value); return this; } property = (property == 'float' ? floatName : property).camelCase(); @@ -3558,6 +3650,11 @@ Element.implement({ value = Math.round(value); } this.style[property] = value; + // + if ((value == '' || value == null) && doesNotRemoveStyles && this.style.removeAttribute){ + this.style.removeAttribute(property); + } + // return this; }, @@ -3579,16 +3676,17 @@ Element.implement({ var color = result.match(/rgba?\([\d\s,]+\)/); if (color) result = result.replace(color[0], color[0].rgbToHex()); } - if (Browser.opera || (Browser.ie && isNaN(parseFloat(result)))){ - if ((/^(height|width)$/).test(property)){ + if (Browser.opera || Browser.ie){ + if ((/^(height|width)$/).test(property) && !(/px$/.test(result))){ var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0; values.each(function(value){ size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt(); }, this); return this['offset' + property.capitalize()] - size + 'px'; } - if (Browser.opera && String(result).indexOf('px') != -1) return result; - if ((/^border(.+)Width|margin|padding/).test(property)) return '0px'; + if (Browser.ie && (/^border(.+)Width|margin|padding/).test(property) && isNaN(parseFloat(result))){ + return '0px'; + } } return result; }, @@ -3940,7 +4038,7 @@ if (!window.addEventListener){ return (this.get('tag') == 'input' && (type == 'radio' || type == 'checkbox')) ? 'propertychange' : 'change' }, condition: function(event){ - return !!(this.type != 'radio' || this.checked); + return this.type != 'radio' || (event.event.propertyName == 'checked' && this.checked); } } } @@ -4641,12 +4739,31 @@ Fx.CSS = new Class({ prepare: function(element, property, values){ values = Array.from(values); - if (values[1] == null){ - values[1] = values[0]; - values[0] = element.getStyle(property); + var from = values[0], to = values[1]; + if (to == null){ + to = from; + from = element.getStyle(property); + var unit = this.options.unit; + // adapted from: https://github.com/ryanmorr/fx/blob/master/fx.js#L299 + if (unit && from.slice(-unit.length) != unit && parseFloat(from) != 0){ + element.setStyle(property, to + unit); + var value = element.getComputedStyle(property); + // IE and Opera support pixelLeft or pixelWidth + if (!(/px$/.test(value))){ + value = element.style[('pixel-' + property).camelCase()]; + if (value == null){ + // adapted from Dean Edwards' http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + var left = element.style.left; + element.style.left = to + unit; + value = element.style.pixelLeft; + element.style.left = left; + } + } + from = (to || 1) / (parseFloat(value) || 1) * (parseFloat(from) || 0); + element.setStyle(property, from + unit); + } } - var parsed = values.map(this.parse); - return {from: parsed[0], to: parsed[1]}; + return {from: this.parse(from), to: this.parse(to)}; }, //parses a value into an array @@ -4834,24 +4951,25 @@ Element.implement({ }, fade: function(how){ - var fade = this.get('tween'), method, to, toggle; - if (how == null) how = 'toggle'; - switch (how){ - case 'in': method = 'start'; to = 1; break; - case 'out': method = 'start'; to = 0; break; - case 'show': method = 'set'; to = 1; break; - case 'hide': method = 'set'; to = 0; break; + var fade = this.get('tween'), method, args = ['opacity'].append(arguments), toggle; + if (args[1] == null) args[1] = 'toggle'; + switch (args[1]){ + case 'in': method = 'start'; args[1] = 1; break; + case 'out': method = 'start'; args[1] = 0; break; + case 'show': method = 'set'; args[1] = 1; break; + case 'hide': method = 'set'; args[1] = 0; break; case 'toggle': var flag = this.retrieve('fade:flag', this.getStyle('opacity') == 1); method = 'start'; - to = flag ? 0 : 1; + args[1] = flag ? 0 : 1; this.store('fade:flag', !flag); toggle = true; break; - default: method = 'start'; to = how; + default: method = 'start'; } if (!toggle) this.eliminate('fade:flag'); - fade[method]('opacity', to); + fade[method].apply(fade, args); + var to = args[args.length - 1]; if (method == 'set' || to != 0) this.setStyle('visibility', to == 0 ? 'hidden' : 'visible'); else fade.chain(function(){ this.element.setStyle('visibility', 'hidden'); diff --git a/couchpotato/static/scripts/library/mootools_more.js b/couchpotato/static/scripts/library/mootools_more.js index e62145e0..d2d70369 100644 --- a/couchpotato/static/scripts/library/mootools_more.js +++ b/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/710fc92ae4753344d23cbd5fc7e44420 -// Or build this file again with packager using: packager build More/Events.Pseudos More/Element.Forms More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical More/Spinner +// Load this file's selection again by visiting: http://mootools.net/more/43db227db7a621ebb062ee621432ae3d +// Or build this file again with packager using: packager build More/Events.Pseudos More/Date More/Date.Extras More/Element.Forms More/Element.Position More/Element.Shortcuts More/Fx.Scroll More/Fx.Slide More/Sortables More/Request.JSONP More/Request.Periodical /* --- @@ -195,6 +195,993 @@ Events.implement(Events.Pseudos(pseudos, proto.addEvent, proto.removeEvent)); })(); +/* +--- + +script: Object.Extras.js + +name: Object.Extras + +description: Extra Object generics, like getFromPath which allows a path notation to child elements. + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - Core/Object + - /MooTools.More + +provides: [Object.Extras] + +... +*/ + +(function(){ + +var defined = function(value){ + return value != null; +}; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + +Object.extend({ + + getFromPath: function(source, parts){ + if (typeof parts == 'string') parts = parts.split('.'); + for (var i = 0, l = parts.length; i < l; i++){ + if (hasOwnProperty.call(source, parts[i])) source = source[parts[i]]; + else return null; + } + return source; + }, + + cleanValues: function(object, method){ + method = method || defined; + for (var key in object) if (!method(object[key])){ + delete object[key]; + } + return object; + }, + + erase: function(object, key){ + if (hasOwnProperty.call(object, key)) delete object[key]; + return object; + }, + + run: function(object){ + var args = Array.slice(arguments, 1); + for (var key in object) if (object[key].apply){ + object[key].apply(object, args); + } + return object; + } + +}); + +})(); + + +/* +--- + +script: Locale.js + +name: Locale + +description: Provides methods for localization. + +license: MIT-style license + +authors: + - Aaron Newton + - Arian Stolwijk + +requires: + - Core/Events + - /Object.Extras + - /MooTools.More + +provides: [Locale, Lang] + +... +*/ + +(function(){ + +var current = null, + locales = {}, + inherits = {}; + +var getSet = function(set){ + if (instanceOf(set, Locale.Set)) return set; + else return locales[set]; +}; + +var Locale = this.Locale = { + + define: function(locale, set, key, value){ + var name; + if (instanceOf(locale, Locale.Set)){ + name = locale.name; + if (name) locales[name] = locale; + } else { + name = locale; + if (!locales[name]) locales[name] = new Locale.Set(name); + locale = locales[name]; + } + + if (set) locale.define(set, key, value); + + + + if (!current) current = locale; + + return locale; + }, + + use: function(locale){ + locale = getSet(locale); + + if (locale){ + current = locale; + + this.fireEvent('change', locale); + + + } + + return this; + }, + + getCurrent: function(){ + return current; + }, + + get: function(key, args){ + return (current) ? current.get(key, args) : ''; + }, + + inherit: function(locale, inherits, set){ + locale = getSet(locale); + + if (locale) locale.inherit(inherits, set); + return this; + }, + + list: function(){ + return Object.keys(locales); + } + +}; + +Object.append(Locale, new Events); + +Locale.Set = new Class({ + + sets: {}, + + inherits: { + locales: [], + sets: {} + }, + + initialize: function(name){ + this.name = name || ''; + }, + + define: function(set, key, value){ + var defineData = this.sets[set]; + if (!defineData) defineData = {}; + + if (key){ + if (typeOf(key) == 'object') defineData = Object.merge(defineData, key); + else defineData[key] = value; + } + this.sets[set] = defineData; + + return this; + }, + + get: function(key, args, _base){ + var value = Object.getFromPath(this.sets, key); + if (value != null){ + var type = typeOf(value); + if (type == 'function') value = value.apply(null, Array.from(args)); + else if (type == 'object') value = Object.clone(value); + return value; + } + + // get value of inherited locales + var index = key.indexOf('.'), + set = index < 0 ? key : key.substr(0, index), + names = (this.inherits.sets[set] || []).combine(this.inherits.locales).include('en-US'); + if (!_base) _base = []; + + for (var i = 0, l = names.length; i < l; i++){ + if (_base.contains(names[i])) continue; + _base.include(names[i]); + + var locale = locales[names[i]]; + if (!locale) continue; + + value = locale.get(key, args, _base); + if (value != null) return value; + } + + return ''; + }, + + inherit: function(names, set){ + names = Array.from(names); + + if (set && !this.inherits.sets[set]) this.inherits.sets[set] = []; + + var l = names.length; + while (l--) (set ? this.inherits.sets[set] : this.inherits.locales).unshift(names[l]); + + return this; + } + +}); + + + +})(); + + +/* +--- + +name: Locale.en-US.Date + +description: Date messages for US English. + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - /Locale + +provides: [Locale.en-US.Date] + +... +*/ + +Locale.define('en-US', 'Date', { + + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + months_abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + days_abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + + // Culture's date order: MM/DD/YYYY + dateOrder: ['month', 'date', 'year'], + shortDate: '%m/%d/%Y', + shortTime: '%I:%M%p', + AM: 'AM', + PM: 'PM', + firstDayOfWeek: 0, + + // Date.Extras + ordinal: function(dayOfMonth){ + // 1st, 2nd, 3rd, etc. + return (dayOfMonth > 3 && dayOfMonth < 21) ? 'th' : ['th', 'st', 'nd', 'rd', 'th'][Math.min(dayOfMonth % 10, 4)]; + }, + + lessThanMinuteAgo: 'less than a minute ago', + minuteAgo: 'about a minute ago', + minutesAgo: '{delta} minutes ago', + hourAgo: 'about an hour ago', + hoursAgo: 'about {delta} hours ago', + dayAgo: '1 day ago', + daysAgo: '{delta} days ago', + weekAgo: '1 week ago', + weeksAgo: '{delta} weeks ago', + monthAgo: '1 month ago', + monthsAgo: '{delta} months ago', + yearAgo: '1 year ago', + yearsAgo: '{delta} years ago', + + lessThanMinuteUntil: 'less than a minute from now', + minuteUntil: 'about a minute from now', + minutesUntil: '{delta} minutes from now', + hourUntil: 'about an hour from now', + hoursUntil: 'about {delta} hours from now', + dayUntil: '1 day from now', + daysUntil: '{delta} days from now', + weekUntil: '1 week from now', + weeksUntil: '{delta} weeks from now', + monthUntil: '1 month from now', + monthsUntil: '{delta} months from now', + yearUntil: '1 year from now', + yearsUntil: '{delta} years from now' + +}); + + +/* +--- + +script: Date.js + +name: Date + +description: Extends the Date native object to include methods useful in managing dates. + +license: MIT-style license + +authors: + - Aaron Newton + - Nicholas Barthelemy - https://svn.nbarthelemy.com/date-js/ + - Harald Kirshner - mail [at] digitarald.de; http://digitarald.de + - Scott Kyle - scott [at] appden.com; http://appden.com + +requires: + - Core/Array + - Core/String + - Core/Number + - MooTools.More + - Locale + - Locale.en-US.Date + +provides: [Date] + +... +*/ + +(function(){ + +var Date = this.Date; + +var DateMethods = Date.Methods = { + ms: 'Milliseconds', + year: 'FullYear', + min: 'Minutes', + mo: 'Month', + sec: 'Seconds', + hr: 'Hours' +}; + +['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds', 'Time', 'TimezoneOffset', + 'Week', 'Timezone', 'GMTOffset', 'DayOfYear', 'LastMonth', 'LastDayOfMonth', 'UTCDate', 'UTCDay', 'UTCFullYear', + 'AMPM', 'Ordinal', 'UTCHours', 'UTCMilliseconds', 'UTCMinutes', 'UTCMonth', 'UTCSeconds', 'UTCMilliseconds'].each(function(method){ + Date.Methods[method.toLowerCase()] = method; +}); + +var pad = function(n, digits, string){ + if (digits == 1) return n; + return n < Math.pow(10, digits - 1) ? (string || '0') + pad(n, digits - 1, string) : n; +}; + +Date.implement({ + + set: function(prop, value){ + prop = prop.toLowerCase(); + var method = DateMethods[prop] && 'set' + DateMethods[prop]; + if (method && this[method]) this[method](value); + return this; + }.overloadSetter(), + + get: function(prop){ + prop = prop.toLowerCase(); + var method = DateMethods[prop] && 'get' + DateMethods[prop]; + if (method && this[method]) return this[method](); + return null; + }.overloadGetter(), + + clone: function(){ + return new Date(this.get('time')); + }, + + increment: function(interval, times){ + interval = interval || 'day'; + times = times != null ? times : 1; + + switch (interval){ + case 'year': + return this.increment('month', times * 12); + case 'month': + var d = this.get('date'); + this.set('date', 1).set('mo', this.get('mo') + times); + return this.set('date', d.min(this.get('lastdayofmonth'))); + case 'week': + return this.increment('day', times * 7); + case 'day': + return this.set('date', this.get('date') + times); + } + + if (!Date.units[interval]) throw new Error(interval + ' is not a supported interval'); + + return this.set('time', this.get('time') + times * Date.units[interval]()); + }, + + decrement: function(interval, times){ + return this.increment(interval, -1 * (times != null ? times : 1)); + }, + + isLeapYear: function(){ + return Date.isLeapYear(this.get('year')); + }, + + clearTime: function(){ + return this.set({hr: 0, min: 0, sec: 0, ms: 0}); + }, + + diff: function(date, resolution){ + if (typeOf(date) == 'string') date = Date.parse(date); + + return ((date - this) / Date.units[resolution || 'day'](3, 3)).round(); // non-leap year, 30-day month + }, + + getLastDayOfMonth: function(){ + return Date.daysInMonth(this.get('mo'), this.get('year')); + }, + + getDayOfYear: function(){ + return (Date.UTC(this.get('year'), this.get('mo'), this.get('date') + 1) + - Date.UTC(this.get('year'), 0, 1)) / Date.units.day(); + }, + + setDay: function(day, firstDayOfWeek){ + if (firstDayOfWeek == null){ + firstDayOfWeek = Date.getMsg('firstDayOfWeek'); + if (firstDayOfWeek === '') firstDayOfWeek = 1; + } + + day = (7 + Date.parseDay(day, true) - firstDayOfWeek) % 7; + var currentDay = (7 + this.get('day') - firstDayOfWeek) % 7; + + return this.increment('day', day - currentDay); + }, + + getWeek: function(firstDayOfWeek){ + if (firstDayOfWeek == null){ + firstDayOfWeek = Date.getMsg('firstDayOfWeek'); + if (firstDayOfWeek === '') firstDayOfWeek = 1; + } + + var date = this, + dayOfWeek = (7 + date.get('day') - firstDayOfWeek) % 7, + dividend = 0, + firstDayOfYear; + + if (firstDayOfWeek == 1){ + // ISO-8601, week belongs to year that has the most days of the week (i.e. has the thursday of the week) + var month = date.get('month'), + startOfWeek = date.get('date') - dayOfWeek; + + if (month == 11 && startOfWeek > 28) return 1; // Week 1 of next year + + if (month == 0 && startOfWeek < -2){ + // Use a date from last year to determine the week + date = new Date(date).decrement('day', dayOfWeek); + dayOfWeek = 0; + } + + firstDayOfYear = new Date(date.get('year'), 0, 1).get('day') || 7; + if (firstDayOfYear > 4) dividend = -7; // First week of the year is not week 1 + } else { + // In other cultures the first week of the year is always week 1 and the last week always 53 or 54. + // Days in the same week can have a different weeknumber if the week spreads across two years. + firstDayOfYear = new Date(date.get('year'), 0, 1).get('day'); + } + + dividend += date.get('dayofyear'); + dividend += 6 - dayOfWeek; // Add days so we calculate the current date's week as a full week + dividend += (7 + firstDayOfYear - firstDayOfWeek) % 7; // Make up for first week of the year not being a full week + + return (dividend / 7); + }, + + getOrdinal: function(day){ + return Date.getMsg('ordinal', day || this.get('date')); + }, + + getTimezone: function(){ + return this.toString() + .replace(/^.*? ([A-Z]{3}).[0-9]{4}.*$/, '$1') + .replace(/^.*?\(([A-Z])[a-z]+ ([A-Z])[a-z]+ ([A-Z])[a-z]+\)$/, '$1$2$3'); + }, + + getGMTOffset: function(){ + var off = this.get('timezoneOffset'); + return ((off > 0) ? '-' : '+') + pad((off.abs() / 60).floor(), 2) + pad(off % 60, 2); + }, + + setAMPM: function(ampm){ + ampm = ampm.toUpperCase(); + var hr = this.get('hr'); + if (hr > 11 && ampm == 'AM') return this.decrement('hour', 12); + else if (hr < 12 && ampm == 'PM') return this.increment('hour', 12); + return this; + }, + + getAMPM: function(){ + return (this.get('hr') < 12) ? 'AM' : 'PM'; + }, + + parse: function(str){ + this.set('time', Date.parse(str)); + return this; + }, + + isValid: function(date){ + if (!date) date = this; + return typeOf(date) == 'date' && !isNaN(date.valueOf()); + }, + + format: function(format){ + if (!this.isValid()) return 'invalid date'; + + if (!format) format = '%x %X'; + if (typeof format == 'string') format = formats[format.toLowerCase()] || format; + if (typeof format == 'function') return format(this); + + var d = this; + return format.replace(/%([a-z%])/gi, + function($0, $1){ + switch ($1){ + case 'a': return Date.getMsg('days_abbr')[d.get('day')]; + case 'A': return Date.getMsg('days')[d.get('day')]; + case 'b': return Date.getMsg('months_abbr')[d.get('month')]; + case 'B': return Date.getMsg('months')[d.get('month')]; + case 'c': return d.format('%a %b %d %H:%M:%S %Y'); + case 'd': return pad(d.get('date'), 2); + case 'e': return pad(d.get('date'), 2, ' '); + case 'H': return pad(d.get('hr'), 2); + case 'I': return pad((d.get('hr') % 12) || 12, 2); + case 'j': return pad(d.get('dayofyear'), 3); + case 'k': return pad(d.get('hr'), 2, ' '); + case 'l': return pad((d.get('hr') % 12) || 12, 2, ' '); + case 'L': return pad(d.get('ms'), 3); + case 'm': return pad((d.get('mo') + 1), 2); + case 'M': return pad(d.get('min'), 2); + case 'o': return d.get('ordinal'); + case 'p': return Date.getMsg(d.get('ampm')); + case 's': return Math.round(d / 1000); + case 'S': return pad(d.get('seconds'), 2); + case 'T': return d.format('%H:%M:%S'); + case 'U': return pad(d.get('week'), 2); + case 'w': return d.get('day'); + case 'x': return d.format(Date.getMsg('shortDate')); + case 'X': return d.format(Date.getMsg('shortTime')); + case 'y': return d.get('year').toString().substr(2); + case 'Y': return d.get('year'); + case 'z': return d.get('GMTOffset'); + case 'Z': return d.get('Timezone'); + } + return $1; + } + ); + }, + + toISOString: function(){ + return this.format('iso8601'); + } + +}).alias({ + toJSON: 'toISOString', + compare: 'diff', + strftime: 'format' +}); + +// The day and month abbreviations are standardized, so we cannot use simply %a and %b because they will get localized +var rfcDayAbbr = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + rfcMonthAbbr = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +var formats = { + db: '%Y-%m-%d %H:%M:%S', + compact: '%Y%m%dT%H%M%S', + 'short': '%d %b %H:%M', + 'long': '%B %d, %Y %H:%M', + rfc822: function(date){ + return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %Z'); + }, + rfc2822: function(date){ + return rfcDayAbbr[date.get('day')] + date.format(', %d ') + rfcMonthAbbr[date.get('month')] + date.format(' %Y %H:%M:%S %z'); + }, + iso8601: function(date){ + return ( + date.getUTCFullYear() + '-' + + pad(date.getUTCMonth() + 1, 2) + '-' + + pad(date.getUTCDate(), 2) + 'T' + + pad(date.getUTCHours(), 2) + ':' + + pad(date.getUTCMinutes(), 2) + ':' + + pad(date.getUTCSeconds(), 2) + '.' + + pad(date.getUTCMilliseconds(), 3) + 'Z' + ); + } +}; + +var parsePatterns = [], + nativeParse = Date.parse; + +var parseWord = function(type, word, num){ + var ret = -1, + translated = Date.getMsg(type + 's'); + switch (typeOf(word)){ + case 'object': + ret = translated[word.get(type)]; + break; + case 'number': + ret = translated[word]; + if (!ret) throw new Error('Invalid ' + type + ' index: ' + word); + break; + case 'string': + var match = translated.filter(function(name){ + return this.test(name); + }, new RegExp('^' + word, 'i')); + if (!match.length) throw new Error('Invalid ' + type + ' string'); + if (match.length > 1) throw new Error('Ambiguous ' + type); + ret = match[0]; + } + + return (num) ? translated.indexOf(ret) : ret; +}; + +var startCentury = 1900, + startYear = 70; + +Date.extend({ + + getMsg: function(key, args){ + return Locale.get('Date.' + key, args); + }, + + units: { + ms: Function.from(1), + second: Function.from(1000), + minute: Function.from(60000), + hour: Function.from(3600000), + day: Function.from(86400000), + week: Function.from(608400000), + month: function(month, year){ + var d = new Date; + return Date.daysInMonth(month != null ? month : d.get('mo'), year != null ? year : d.get('year')) * 86400000; + }, + year: function(year){ + year = year || new Date().get('year'); + return Date.isLeapYear(year) ? 31622400000 : 31536000000; + } + }, + + daysInMonth: function(month, year){ + return [31, Date.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; + }, + + isLeapYear: function(year){ + return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0); + }, + + parse: function(from){ + var t = typeOf(from); + if (t == 'number') return new Date(from); + if (t != 'string') return from; + from = from.clean(); + if (!from.length) return null; + + var parsed; + parsePatterns.some(function(pattern){ + var bits = pattern.re.exec(from); + return (bits) ? (parsed = pattern.handler(bits)) : false; + }); + + if (!(parsed && parsed.isValid())){ + parsed = new Date(nativeParse(from)); + if (!(parsed && parsed.isValid())) parsed = new Date(from.toInt()); + } + return parsed; + }, + + parseDay: function(day, num){ + return parseWord('day', day, num); + }, + + parseMonth: function(month, num){ + return parseWord('month', month, num); + }, + + parseUTC: function(value){ + var localDate = new Date(value); + var utcSeconds = Date.UTC( + localDate.get('year'), + localDate.get('mo'), + localDate.get('date'), + localDate.get('hr'), + localDate.get('min'), + localDate.get('sec'), + localDate.get('ms') + ); + return new Date(utcSeconds); + }, + + orderIndex: function(unit){ + return Date.getMsg('dateOrder').indexOf(unit) + 1; + }, + + defineFormat: function(name, format){ + formats[name] = format; + return this; + }, + + + + defineParser: function(pattern){ + parsePatterns.push((pattern.re && pattern.handler) ? pattern : build(pattern)); + return this; + }, + + defineParsers: function(){ + Array.flatten(arguments).each(Date.defineParser); + return this; + }, + + define2DigitYearStart: function(year){ + startYear = year % 100; + startCentury = year - startYear; + return this; + } + +}).extend({ + defineFormats: Date.defineFormat.overloadSetter() +}); + +var regexOf = function(type){ + return new RegExp('(?:' + Date.getMsg(type).map(function(name){ + return name.substr(0, 3); + }).join('|') + ')[a-z]*'); +}; + +var replacers = function(key){ + switch (key){ + case 'T': + return '%H:%M:%S'; + case 'x': // iso8601 covers yyyy-mm-dd, so just check if month is first + return ((Date.orderIndex('month') == 1) ? '%m[-./]%d' : '%d[-./]%m') + '([-./]%y)?'; + case 'X': + return '%H([.:]%M)?([.:]%S([.:]%s)?)? ?%p? ?%z?'; + } + return null; +}; + +var keys = { + d: /[0-2]?[0-9]|3[01]/, + H: /[01]?[0-9]|2[0-3]/, + I: /0?[1-9]|1[0-2]/, + M: /[0-5]?\d/, + s: /\d+/, + o: /[a-z]*/, + p: /[ap]\.?m\.?/, + y: /\d{2}|\d{4}/, + Y: /\d{4}/, + z: /Z|[+-]\d{2}(?::?\d{2})?/ +}; + +keys.m = keys.I; +keys.S = keys.M; + +var currentLanguage; + +var recompile = function(language){ + currentLanguage = language; + + keys.a = keys.A = regexOf('days'); + keys.b = keys.B = regexOf('months'); + + parsePatterns.each(function(pattern, i){ + if (pattern.format) parsePatterns[i] = build(pattern.format); + }); +}; + +var build = function(format){ + if (!currentLanguage) return {format: format}; + + var parsed = []; + var re = (format.source || format) // allow format to be regex + .replace(/%([a-z])/gi, + function($0, $1){ + return replacers($1) || $0; + } + ).replace(/\((?!\?)/g, '(?:') // make all groups non-capturing + .replace(/ (?!\?|\*)/g, ',? ') // be forgiving with spaces and commas + .replace(/%([a-z%])/gi, + function($0, $1){ + var p = keys[$1]; + if (!p) return $1; + parsed.push($1); + return '(' + p.source + ')'; + } + ).replace(/\[a-z\]/gi, '[a-z\\u00c0-\\uffff;\&]'); // handle unicode words + + return { + format: format, + re: new RegExp('^' + re + '$', 'i'), + handler: function(bits){ + bits = bits.slice(1).associate(parsed); + var date = new Date().clearTime(), + year = bits.y || bits.Y; + + if (year != null) handle.call(date, 'y', year); // need to start in the right year + if ('d' in bits) handle.call(date, 'd', 1); + if ('m' in bits || bits.b || bits.B) handle.call(date, 'm', 1); + + for (var key in bits) handle.call(date, key, bits[key]); + return date; + } + }; +}; + +var handle = function(key, value){ + if (!value) return this; + + switch (key){ + case 'a': case 'A': return this.set('day', Date.parseDay(value, true)); + case 'b': case 'B': return this.set('mo', Date.parseMonth(value, true)); + case 'd': return this.set('date', value); + case 'H': case 'I': return this.set('hr', value); + case 'm': return this.set('mo', value - 1); + case 'M': return this.set('min', value); + case 'p': return this.set('ampm', value.replace(/\./g, '')); + case 'S': return this.set('sec', value); + case 's': return this.set('ms', ('0.' + value) * 1000); + case 'w': return this.set('day', value); + case 'Y': return this.set('year', value); + case 'y': + value = +value; + if (value < 100) value += startCentury + (value < startYear ? 100 : 0); + return this.set('year', value); + case 'z': + if (value == 'Z') value = '+00'; + var offset = value.match(/([+-])(\d{2}):?(\d{2})?/); + offset = (offset[1] + '1') * (offset[2] * 60 + (+offset[3] || 0)) + this.getTimezoneOffset(); + return this.set('time', this - offset * 60000); + } + + return this; +}; + +Date.defineParsers( + '%Y([-./]%m([-./]%d((T| )%X)?)?)?', // "1999-12-31", "1999-12-31 11:59pm", "1999-12-31 23:59:59", ISO8601 + '%Y%m%d(T%H(%M%S?)?)?', // "19991231", "19991231T1159", compact + '%x( %X)?', // "12/31", "12.31.99", "12-31-1999", "12/31/2008 11:59 PM" + '%d%o( %b( %Y)?)?( %X)?', // "31st", "31st December", "31 Dec 1999", "31 Dec 1999 11:59pm" + '%b( %d%o)?( %Y)?( %X)?', // Same as above with month and day switched + '%Y %b( %d%o( %X)?)?', // Same as above with year coming first + '%o %b %d %X %z %Y', // "Thu Oct 22 08:11:23 +0000 2009" + '%T', // %H:%M:%S + '%H:%M( ?%p)?' // "11:05pm", "11:05 am" and "11:05" +); + +Locale.addEvent('change', function(language){ + if (Locale.get('Date')) recompile(language); +}).fireEvent('change', Locale.getCurrent()); + +})(); + + +/* +--- + +script: Date.Extras.js + +name: Date.Extras + +description: Extends the Date native object to include extra methods (on top of those in Date.js). + +license: MIT-style license + +authors: + - Aaron Newton + - Scott Kyle + +requires: + - /Date + +provides: [Date.Extras] + +... +*/ + +Date.implement({ + + timeDiffInWords: function(to){ + return Date.distanceOfTimeInWords(this, to || new Date); + }, + + timeDiff: function(to, separator){ + if (to == null) to = new Date; + var delta = ((to - this) / 1000).floor().abs(); + + var vals = [], + durations = [60, 60, 24, 365, 0], + names = ['s', 'm', 'h', 'd', 'y'], + value, duration; + + for (var item = 0; item < durations.length; item++){ + if (item && !delta) break; + value = delta; + if ((duration = durations[item])){ + value = (delta % duration); + delta = (delta / duration).floor(); + } + vals.unshift(value + (names[item] || '')); + } + + return vals.join(separator || ':'); + } + +}).extend({ + + distanceOfTimeInWords: function(from, to){ + return Date.getTimePhrase(((to - from) / 1000).toInt()); + }, + + getTimePhrase: function(delta){ + var suffix = (delta < 0) ? 'Until' : 'Ago'; + if (delta < 0) delta *= -1; + + var units = { + minute: 60, + hour: 60, + day: 24, + week: 7, + month: 52 / 12, + year: 12, + eon: Infinity + }; + + var msg = 'lessThanMinute'; + + for (var unit in units){ + var interval = units[unit]; + if (delta < 1.5 * interval){ + if (delta > 0.75 * interval) msg = unit; + break; + } + delta /= interval; + msg = unit + 's'; + } + + delta = delta.round(); + return Date.getMsg(msg + suffix, delta).substitute({delta: delta}); + } + +}).defineParsers( + + { + // "today", "tomorrow", "yesterday" + re: /^(?:tod|tom|yes)/i, + handler: function(bits){ + var d = new Date().clearTime(); + switch (bits[0]){ + case 'tom': return d.increment(); + case 'yes': return d.decrement(); + default: return d; + } + } + }, + + { + // "next Wednesday", "last Thursday" + re: /^(next|last) ([a-z]+)$/i, + handler: function(bits){ + var d = new Date().clearTime(); + var day = d.getDay(); + var newDay = Date.parseDay(bits[2], true); + var addDays = newDay - day; + if (newDay <= day) addDays += 7; + if (bits[1] == 'last') addDays -= 7; + return d.set('date', d.getDate() + addDays); + } + } + +).alias('timeAgoInWords', 'timeDiffInWords'); + + /* --- @@ -487,6 +1474,412 @@ Element.implement({ }); +/* +--- + +script: Element.Measure.js + +name: Element.Measure + +description: Extends the Element native object to include methods useful in measuring dimensions. + +credits: "Element.measure / .expose methods by Daniel Steigerwald License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz" + +license: MIT-style license + +authors: + - Aaron Newton + +requires: + - Core/Element.Style + - Core/Element.Dimensions + - /MooTools.More + +provides: [Element.Measure] + +... +*/ + +(function(){ + +var getStylesList = function(styles, planes){ + var list = []; + Object.each(planes, function(directions){ + Object.each(directions, function(edge){ + styles.each(function(style){ + list.push(style + '-' + edge + (style == 'border' ? '-width' : '')); + }); + }); + }); + return list; +}; + +var calculateEdgeSize = function(edge, styles){ + var total = 0; + Object.each(styles, function(value, style){ + if (style.test(edge)) total = total + value.toInt(); + }); + return total; +}; + +var isVisible = function(el){ + return !!(!el || el.offsetHeight || el.offsetWidth); +}; + + +Element.implement({ + + measure: function(fn){ + if (isVisible(this)) return fn.call(this); + var parent = this.getParent(), + toMeasure = []; + while (!isVisible(parent) && parent != document.body){ + toMeasure.push(parent.expose()); + parent = parent.getParent(); + } + var restore = this.expose(), + result = fn.call(this); + restore(); + toMeasure.each(function(restore){ + restore(); + }); + return result; + }, + + expose: function(){ + if (this.getStyle('display') != 'none') return function(){}; + var before = this.style.cssText; + this.setStyles({ + display: 'block', + position: 'absolute', + visibility: 'hidden' + }); + return function(){ + this.style.cssText = before; + }.bind(this); + }, + + getDimensions: function(options){ + options = Object.merge({computeSize: false}, options); + var dim = {x: 0, y: 0}; + + var getSize = function(el, options){ + return (options.computeSize) ? el.getComputedSize(options) : el.getSize(); + }; + + var parent = this.getParent('body'); + + if (parent && this.getStyle('display') == 'none'){ + dim = this.measure(function(){ + return getSize(this, options); + }); + } else if (parent){ + try { //safari sometimes crashes here, so catch it + dim = getSize(this, options); + }catch(e){} + } + + return Object.append(dim, (dim.x || dim.x === 0) ? { + width: dim.x, + height: dim.y + } : { + x: dim.width, + y: dim.height + } + ); + }, + + getComputedSize: function(options){ + + + options = Object.merge({ + styles: ['padding','border'], + planes: { + height: ['top','bottom'], + width: ['left','right'] + }, + mode: 'both' + }, options); + + var styles = {}, + size = {width: 0, height: 0}, + dimensions; + + if (options.mode == 'vertical'){ + delete size.width; + delete options.planes.width; + } else if (options.mode == 'horizontal'){ + delete size.height; + delete options.planes.height; + } + + getStylesList(options.styles, options.planes).each(function(style){ + styles[style] = this.getStyle(style).toInt(); + }, this); + + Object.each(options.planes, function(edges, plane){ + + var capitalized = plane.capitalize(), + style = this.getStyle(plane); + + if (style == 'auto' && !dimensions) dimensions = this.getDimensions(); + + style = styles[plane] = (style == 'auto') ? dimensions[plane] : style.toInt(); + size['total' + capitalized] = style; + + edges.each(function(edge){ + var edgesize = calculateEdgeSize(edge, styles); + size['computed' + edge.capitalize()] = edgesize; + size['total' + capitalized] += edgesize; + }); + + }, this); + + return Object.append(size, styles); + } + +}); + +})(); + + +/* +--- + +script: Element.Position.js + +name: Element.Position + +description: Extends the Element native object to include methods useful positioning elements relative to others. + +license: MIT-style license + +authors: + - Aaron Newton + - Jacob Thornton + +requires: + - Core/Options + - Core/Element.Dimensions + - Element.Measure + +provides: [Element.Position] + +... +*/ + +(function(original){ + +var local = Element.Position = { + + options: {/* + edge: false, + returnPos: false, + minimum: {x: 0, y: 0}, + maximum: {x: 0, y: 0}, + relFixedPosition: false, + ignoreMargins: false, + ignoreScroll: false, + allowNegative: false,*/ + relativeTo: document.body, + position: { + x: 'center', //left, center, right + y: 'center' //top, center, bottom + }, + offset: {x: 0, y: 0} + }, + + getOptions: function(element, options){ + options = Object.merge({}, local.options, options); + local.setPositionOption(options); + local.setEdgeOption(options); + local.setOffsetOption(element, options); + local.setDimensionsOption(element, options); + return options; + }, + + setPositionOption: function(options){ + options.position = local.getCoordinateFromValue(options.position); + }, + + setEdgeOption: function(options){ + var edgeOption = local.getCoordinateFromValue(options.edge); + options.edge = edgeOption ? edgeOption : + (options.position.x == 'center' && options.position.y == 'center') ? {x: 'center', y: 'center'} : + {x: 'left', y: 'top'}; + }, + + setOffsetOption: function(element, options){ + var parentOffset = {x: 0, y: 0}, + offsetParent = element.measure(function(){ + return document.id(this.getOffsetParent()); + }), + parentScroll = offsetParent.getScroll(); + + if (!offsetParent || offsetParent == element.getDocument().body) return; + parentOffset = offsetParent.measure(function(){ + var position = this.getPosition(); + if (this.getStyle('position') == 'fixed'){ + var scroll = window.getScroll(); + position.x += scroll.x; + position.y += scroll.y; + } + return position; + }); + + options.offset = { + parentPositioned: offsetParent != document.id(options.relativeTo), + x: options.offset.x - parentOffset.x + parentScroll.x, + y: options.offset.y - parentOffset.y + parentScroll.y + }; + }, + + setDimensionsOption: function(element, options){ + options.dimensions = element.getDimensions({ + computeSize: true, + styles: ['padding', 'border', 'margin'] + }); + }, + + getPosition: function(element, options){ + var position = {}; + options = local.getOptions(element, options); + var relativeTo = document.id(options.relativeTo) || document.body; + + local.setPositionCoordinates(options, position, relativeTo); + if (options.edge) local.toEdge(position, options); + + var offset = options.offset; + position.left = ((position.x >= 0 || offset.parentPositioned || options.allowNegative) ? position.x : 0).toInt(); + position.top = ((position.y >= 0 || offset.parentPositioned || options.allowNegative) ? position.y : 0).toInt(); + + local.toMinMax(position, options); + + if (options.relFixedPosition || relativeTo.getStyle('position') == 'fixed') local.toRelFixedPosition(relativeTo, position); + if (options.ignoreScroll) local.toIgnoreScroll(relativeTo, position); + if (options.ignoreMargins) local.toIgnoreMargins(position, options); + + position.left = Math.ceil(position.left); + position.top = Math.ceil(position.top); + delete position.x; + delete position.y; + + return position; + }, + + setPositionCoordinates: function(options, position, relativeTo){ + var offsetY = options.offset.y, + offsetX = options.offset.x, + calc = (relativeTo == document.body) ? window.getScroll() : relativeTo.getPosition(), + top = calc.y, + left = calc.x, + winSize = window.getSize(); + + switch(options.position.x){ + case 'left': position.x = left + offsetX; break; + case 'right': position.x = left + offsetX + relativeTo.offsetWidth; break; + default: position.x = left + ((relativeTo == document.body ? winSize.x : relativeTo.offsetWidth) / 2) + offsetX; break; + } + + switch(options.position.y){ + case 'top': position.y = top + offsetY; break; + case 'bottom': position.y = top + offsetY + relativeTo.offsetHeight; break; + default: position.y = top + ((relativeTo == document.body ? winSize.y : relativeTo.offsetHeight) / 2) + offsetY; break; + } + }, + + toMinMax: function(position, options){ + var xy = {left: 'x', top: 'y'}, value; + ['minimum', 'maximum'].each(function(minmax){ + ['left', 'top'].each(function(lr){ + value = options[minmax] ? options[minmax][xy[lr]] : null; + if (value != null && ((minmax == 'minimum') ? position[lr] < value : position[lr] > value)) position[lr] = value; + }); + }); + }, + + toRelFixedPosition: function(relativeTo, position){ + var winScroll = window.getScroll(); + position.top += winScroll.y; + position.left += winScroll.x; + }, + + toIgnoreScroll: function(relativeTo, position){ + var relScroll = relativeTo.getScroll(); + position.top -= relScroll.y; + position.left -= relScroll.x; + }, + + toIgnoreMargins: function(position, options){ + position.left += options.edge.x == 'right' + ? options.dimensions['margin-right'] + : (options.edge.x != 'center' + ? -options.dimensions['margin-left'] + : -options.dimensions['margin-left'] + ((options.dimensions['margin-right'] + options.dimensions['margin-left']) / 2)); + + position.top += options.edge.y == 'bottom' + ? options.dimensions['margin-bottom'] + : (options.edge.y != 'center' + ? -options.dimensions['margin-top'] + : -options.dimensions['margin-top'] + ((options.dimensions['margin-bottom'] + options.dimensions['margin-top']) / 2)); + }, + + toEdge: function(position, options){ + var edgeOffset = {}, + dimensions = options.dimensions, + edge = options.edge; + + switch(edge.x){ + case 'left': edgeOffset.x = 0; break; + case 'right': edgeOffset.x = -dimensions.x - dimensions.computedRight - dimensions.computedLeft; break; + // center + default: edgeOffset.x = -(Math.round(dimensions.totalWidth / 2)); break; + } + + switch(edge.y){ + case 'top': edgeOffset.y = 0; break; + case 'bottom': edgeOffset.y = -dimensions.y - dimensions.computedTop - dimensions.computedBottom; break; + // center + default: edgeOffset.y = -(Math.round(dimensions.totalHeight / 2)); break; + } + + position.x += edgeOffset.x; + position.y += edgeOffset.y; + }, + + getCoordinateFromValue: function(option){ + if (typeOf(option) != 'string') return option; + option = option.toLowerCase(); + + return { + x: option.test('left') ? 'left' + : (option.test('right') ? 'right' : 'center'), + y: option.test(/upper|top/) ? 'top' + : (option.test('bottom') ? 'bottom' : 'center') + }; + } + +}; + +Element.implement({ + + position: function(options){ + if (options && (options.x != null || options.y != null)){ + return (original ? original.apply(this, arguments) : this); + } + var position = this.setStyle('position', 'absolute').calculatePosition(options); + return (options && options.returnPos) ? position : this.setStyles(position); + }, + + calculatePosition: function(options){ + return local.getPosition(this, options); + } + +}); + +})(Element.prototype.position); + + /* --- @@ -1768,1094 +3161,3 @@ Request.implement({ }); - -/* ---- - -script: Class.Refactor.js - -name: Class.Refactor - -description: Extends a class onto itself with new property, preserving any items attached to the class's namespace. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Class - - /MooTools.More - -# Some modules declare themselves dependent on Class.Refactor -provides: [Class.refactor, Class.Refactor] - -... -*/ - -Class.refactor = function(original, refactors){ - - Object.each(refactors, function(item, name){ - var origin = original.prototype[name]; - origin = (origin && origin.$origin) || origin || function(){}; - original.implement(name, (typeof item == 'function') ? function(){ - var old = this.previous; - this.previous = origin; - var value = item.apply(this, arguments); - this.previous = old; - return value; - } : item); - }); - - return original; - -}; - - -/* ---- - -script: Class.Binds.js - -name: Class.Binds - -description: Automagically binds specified methods in a class to the instance of the class. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Class - - /MooTools.More - -provides: [Class.Binds] - -... -*/ - -Class.Mutators.Binds = function(binds){ - if (!this.prototype.initialize) this.implement('initialize', function(){}); - return Array.from(binds).concat(this.prototype.Binds || []); -}; - -Class.Mutators.initialize = function(initialize){ - return function(){ - Array.from(this.Binds).each(function(name){ - var original = this[name]; - if (original) this[name] = original.bind(this); - }, this); - return initialize.apply(this, arguments); - }; -}; - - -/* ---- - -script: Element.Measure.js - -name: Element.Measure - -description: Extends the Element native object to include methods useful in measuring dimensions. - -credits: "Element.measure / .expose methods by Daniel Steigerwald License: MIT-style license. Copyright: Copyright (c) 2008 Daniel Steigerwald, daniel.steigerwald.cz" - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Element.Style - - Core/Element.Dimensions - - /MooTools.More - -provides: [Element.Measure] - -... -*/ - -(function(){ - -var getStylesList = function(styles, planes){ - var list = []; - Object.each(planes, function(directions){ - Object.each(directions, function(edge){ - styles.each(function(style){ - list.push(style + '-' + edge + (style == 'border' ? '-width' : '')); - }); - }); - }); - return list; -}; - -var calculateEdgeSize = function(edge, styles){ - var total = 0; - Object.each(styles, function(value, style){ - if (style.test(edge)) total = total + value.toInt(); - }); - return total; -}; - -var isVisible = function(el){ - return !!(!el || el.offsetHeight || el.offsetWidth); -}; - - -Element.implement({ - - measure: function(fn){ - if (isVisible(this)) return fn.call(this); - var parent = this.getParent(), - toMeasure = []; - while (!isVisible(parent) && parent != document.body){ - toMeasure.push(parent.expose()); - parent = parent.getParent(); - } - var restore = this.expose(), - result = fn.call(this); - restore(); - toMeasure.each(function(restore){ - restore(); - }); - return result; - }, - - expose: function(){ - if (this.getStyle('display') != 'none') return function(){}; - var before = this.style.cssText; - this.setStyles({ - display: 'block', - position: 'absolute', - visibility: 'hidden' - }); - return function(){ - this.style.cssText = before; - }.bind(this); - }, - - getDimensions: function(options){ - options = Object.merge({computeSize: false}, options); - var dim = {x: 0, y: 0}; - - var getSize = function(el, options){ - return (options.computeSize) ? el.getComputedSize(options) : el.getSize(); - }; - - var parent = this.getParent('body'); - - if (parent && this.getStyle('display') == 'none'){ - dim = this.measure(function(){ - return getSize(this, options); - }); - } else if (parent){ - try { //safari sometimes crashes here, so catch it - dim = getSize(this, options); - }catch(e){} - } - - return Object.append(dim, (dim.x || dim.x === 0) ? { - width: dim.x, - height: dim.y - } : { - x: dim.width, - y: dim.height - } - ); - }, - - getComputedSize: function(options){ - - - options = Object.merge({ - styles: ['padding','border'], - planes: { - height: ['top','bottom'], - width: ['left','right'] - }, - mode: 'both' - }, options); - - var styles = {}, - size = {width: 0, height: 0}, - dimensions; - - if (options.mode == 'vertical'){ - delete size.width; - delete options.planes.width; - } else if (options.mode == 'horizontal'){ - delete size.height; - delete options.planes.height; - } - - getStylesList(options.styles, options.planes).each(function(style){ - styles[style] = this.getStyle(style).toInt(); - }, this); - - Object.each(options.planes, function(edges, plane){ - - var capitalized = plane.capitalize(), - style = this.getStyle(plane); - - if (style == 'auto' && !dimensions) dimensions = this.getDimensions(); - - style = styles[plane] = (style == 'auto') ? dimensions[plane] : style.toInt(); - size['total' + capitalized] = style; - - edges.each(function(edge){ - var edgesize = calculateEdgeSize(edge, styles); - size['computed' + edge.capitalize()] = edgesize; - size['total' + capitalized] += edgesize; - }); - - }, this); - - return Object.append(size, styles); - } - -}); - -})(); - - -/* ---- - -script: Element.Position.js - -name: Element.Position - -description: Extends the Element native object to include methods useful positioning elements relative to others. - -license: MIT-style license - -authors: - - Aaron Newton - - Jacob Thornton - -requires: - - Core/Options - - Core/Element.Dimensions - - Element.Measure - -provides: [Element.Position] - -... -*/ - -(function(original){ - -var local = Element.Position = { - - options: {/* - edge: false, - returnPos: false, - minimum: {x: 0, y: 0}, - maximum: {x: 0, y: 0}, - relFixedPosition: false, - ignoreMargins: false, - ignoreScroll: false, - allowNegative: false,*/ - relativeTo: document.body, - position: { - x: 'center', //left, center, right - y: 'center' //top, center, bottom - }, - offset: {x: 0, y: 0} - }, - - getOptions: function(element, options){ - options = Object.merge({}, local.options, options); - local.setPositionOption(options); - local.setEdgeOption(options); - local.setOffsetOption(element, options); - local.setDimensionsOption(element, options); - return options; - }, - - setPositionOption: function(options){ - options.position = local.getCoordinateFromValue(options.position); - }, - - setEdgeOption: function(options){ - var edgeOption = local.getCoordinateFromValue(options.edge); - options.edge = edgeOption ? edgeOption : - (options.position.x == 'center' && options.position.y == 'center') ? {x: 'center', y: 'center'} : - {x: 'left', y: 'top'}; - }, - - setOffsetOption: function(element, options){ - var parentOffset = {x: 0, y: 0}, - offsetParent = element.measure(function(){ - return document.id(this.getOffsetParent()); - }), - parentScroll = offsetParent.getScroll(); - - if (!offsetParent || offsetParent == element.getDocument().body) return; - parentOffset = offsetParent.measure(function(){ - var position = this.getPosition(); - if (this.getStyle('position') == 'fixed'){ - var scroll = window.getScroll(); - position.x += scroll.x; - position.y += scroll.y; - } - return position; - }); - - options.offset = { - parentPositioned: offsetParent != document.id(options.relativeTo), - x: options.offset.x - parentOffset.x + parentScroll.x, - y: options.offset.y - parentOffset.y + parentScroll.y - }; - }, - - setDimensionsOption: function(element, options){ - options.dimensions = element.getDimensions({ - computeSize: true, - styles: ['padding', 'border', 'margin'] - }); - }, - - getPosition: function(element, options){ - var position = {}; - options = local.getOptions(element, options); - var relativeTo = document.id(options.relativeTo) || document.body; - - local.setPositionCoordinates(options, position, relativeTo); - if (options.edge) local.toEdge(position, options); - - var offset = options.offset; - position.left = ((position.x >= 0 || offset.parentPositioned || options.allowNegative) ? position.x : 0).toInt(); - position.top = ((position.y >= 0 || offset.parentPositioned || options.allowNegative) ? position.y : 0).toInt(); - - local.toMinMax(position, options); - - if (options.relFixedPosition || relativeTo.getStyle('position') == 'fixed') local.toRelFixedPosition(relativeTo, position); - if (options.ignoreScroll) local.toIgnoreScroll(relativeTo, position); - if (options.ignoreMargins) local.toIgnoreMargins(position, options); - - position.left = Math.ceil(position.left); - position.top = Math.ceil(position.top); - delete position.x; - delete position.y; - - return position; - }, - - setPositionCoordinates: function(options, position, relativeTo){ - var offsetY = options.offset.y, - offsetX = options.offset.x, - calc = (relativeTo == document.body) ? window.getScroll() : relativeTo.getPosition(), - top = calc.y, - left = calc.x, - winSize = window.getSize(); - - switch(options.position.x){ - case 'left': position.x = left + offsetX; break; - case 'right': position.x = left + offsetX + relativeTo.offsetWidth; break; - default: position.x = left + ((relativeTo == document.body ? winSize.x : relativeTo.offsetWidth) / 2) + offsetX; break; - } - - switch(options.position.y){ - case 'top': position.y = top + offsetY; break; - case 'bottom': position.y = top + offsetY + relativeTo.offsetHeight; break; - default: position.y = top + ((relativeTo == document.body ? winSize.y : relativeTo.offsetHeight) / 2) + offsetY; break; - } - }, - - toMinMax: function(position, options){ - var xy = {left: 'x', top: 'y'}, value; - ['minimum', 'maximum'].each(function(minmax){ - ['left', 'top'].each(function(lr){ - value = options[minmax] ? options[minmax][xy[lr]] : null; - if (value != null && ((minmax == 'minimum') ? position[lr] < value : position[lr] > value)) position[lr] = value; - }); - }); - }, - - toRelFixedPosition: function(relativeTo, position){ - var winScroll = window.getScroll(); - position.top += winScroll.y; - position.left += winScroll.x; - }, - - toIgnoreScroll: function(relativeTo, position){ - var relScroll = relativeTo.getScroll(); - position.top -= relScroll.y; - position.left -= relScroll.x; - }, - - toIgnoreMargins: function(position, options){ - position.left += options.edge.x == 'right' - ? options.dimensions['margin-right'] - : (options.edge.x != 'center' - ? -options.dimensions['margin-left'] - : -options.dimensions['margin-left'] + ((options.dimensions['margin-right'] + options.dimensions['margin-left']) / 2)); - - position.top += options.edge.y == 'bottom' - ? options.dimensions['margin-bottom'] - : (options.edge.y != 'center' - ? -options.dimensions['margin-top'] - : -options.dimensions['margin-top'] + ((options.dimensions['margin-bottom'] + options.dimensions['margin-top']) / 2)); - }, - - toEdge: function(position, options){ - var edgeOffset = {}, - dimensions = options.dimensions, - edge = options.edge; - - switch(edge.x){ - case 'left': edgeOffset.x = 0; break; - case 'right': edgeOffset.x = -dimensions.x - dimensions.computedRight - dimensions.computedLeft; break; - // center - default: edgeOffset.x = -(Math.round(dimensions.totalWidth / 2)); break; - } - - switch(edge.y){ - case 'top': edgeOffset.y = 0; break; - case 'bottom': edgeOffset.y = -dimensions.y - dimensions.computedTop - dimensions.computedBottom; break; - // center - default: edgeOffset.y = -(Math.round(dimensions.totalHeight / 2)); break; - } - - position.x += edgeOffset.x; - position.y += edgeOffset.y; - }, - - getCoordinateFromValue: function(option){ - if (typeOf(option) != 'string') return option; - option = option.toLowerCase(); - - return { - x: option.test('left') ? 'left' - : (option.test('right') ? 'right' : 'center'), - y: option.test(/upper|top/) ? 'top' - : (option.test('bottom') ? 'bottom' : 'center') - }; - } - -}; - -Element.implement({ - - position: function(options){ - if (options && (options.x != null || options.y != null)){ - return (original ? original.apply(this, arguments) : this); - } - var position = this.setStyle('position', 'absolute').calculatePosition(options); - return (options && options.returnPos) ? position : this.setStyles(position); - }, - - calculatePosition: function(options){ - return local.getPosition(this, options); - } - -}); - -})(Element.prototype.position); - - -/* ---- - -script: Class.Occlude.js - -name: Class.Occlude - -description: Prevents a class from being applied to a DOM element twice. - -license: MIT-style license. - -authors: - - Aaron Newton - -requires: - - Core/Class - - Core/Element - - /MooTools.More - -provides: [Class.Occlude] - -... -*/ - -Class.Occlude = new Class({ - - occlude: function(property, element){ - element = document.id(element || this.element); - var instance = element.retrieve(property || this.property); - if (instance && !this.occluded) - return (this.occluded = instance); - - this.occluded = false; - element.store(property || this.property, this); - return this.occluded; - } - -}); - - -/* ---- - -script: IframeShim.js - -name: IframeShim - -description: Defines IframeShim, a class for obscuring select lists and flash objects in IE. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Element.Event - - Core/Element.Style - - Core/Options - - Core/Events - - /Element.Position - - /Class.Occlude - -provides: [IframeShim] - -... -*/ - -var IframeShim = new Class({ - - Implements: [Options, Events, Class.Occlude], - - options: { - className: 'iframeShim', - src: 'javascript:false;document.write("");', - display: false, - zIndex: null, - margin: 0, - offset: {x: 0, y: 0}, - browsers: (Browser.ie6 || (Browser.firefox && Browser.version < 3 && Browser.Platform.mac)) - }, - - property: 'IframeShim', - - initialize: function(element, options){ - this.element = document.id(element); - if (this.occlude()) return this.occluded; - this.setOptions(options); - this.makeShim(); - return this; - }, - - makeShim: function(){ - if (this.options.browsers){ - var zIndex = this.element.getStyle('zIndex').toInt(); - - if (!zIndex){ - zIndex = 1; - var pos = this.element.getStyle('position'); - if (pos == 'static' || !pos) this.element.setStyle('position', 'relative'); - this.element.setStyle('zIndex', zIndex); - } - zIndex = ((this.options.zIndex != null || this.options.zIndex === 0) && zIndex > this.options.zIndex) ? this.options.zIndex : zIndex - 1; - if (zIndex < 0) zIndex = 1; - this.shim = new Element('iframe', { - src: this.options.src, - scrolling: 'no', - frameborder: 0, - styles: { - zIndex: zIndex, - position: 'absolute', - border: 'none', - filter: 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)' - }, - 'class': this.options.className - }).store('IframeShim', this); - var inject = (function(){ - this.shim.inject(this.element, 'after'); - this[this.options.display ? 'show' : 'hide'](); - this.fireEvent('inject'); - }).bind(this); - if (!IframeShim.ready) window.addEvent('load', inject); - else inject(); - } else { - this.position = this.hide = this.show = this.dispose = Function.from(this); - } - }, - - position: function(){ - if (!IframeShim.ready || !this.shim) return this; - var size = this.element.measure(function(){ - return this.getSize(); - }); - if (this.options.margin != undefined){ - size.x = size.x - (this.options.margin * 2); - size.y = size.y - (this.options.margin * 2); - this.options.offset.x += this.options.margin; - this.options.offset.y += this.options.margin; - } - this.shim.set({width: size.x, height: size.y}).position({ - relativeTo: this.element, - offset: this.options.offset - }); - return this; - }, - - hide: function(){ - if (this.shim) this.shim.setStyle('display', 'none'); - return this; - }, - - show: function(){ - if (this.shim) this.shim.setStyle('display', 'block'); - return this.position(); - }, - - dispose: function(){ - if (this.shim) this.shim.dispose(); - return this; - }, - - destroy: function(){ - if (this.shim) this.shim.destroy(); - return this; - } - -}); - -window.addEvent('load', function(){ - IframeShim.ready = true; -}); - - -/* ---- - -script: Mask.js - -name: Mask - -description: Creates a mask element to cover another. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Options - - Core/Events - - Core/Element.Event - - /Class.Binds - - /Element.Position - - /IframeShim - -provides: [Mask] - -... -*/ - -var Mask = new Class({ - - Implements: [Options, Events], - - Binds: ['position'], - - options: {/* - onShow: function(){}, - onHide: function(){}, - onDestroy: function(){}, - onClick: function(event){}, - inject: { - where: 'after', - target: null, - }, - hideOnClick: false, - id: null, - destroyOnHide: false,*/ - style: {}, - 'class': 'mask', - maskMargins: false, - useIframeShim: true, - iframeShimOptions: {} - }, - - initialize: function(target, options){ - this.target = document.id(target) || document.id(document.body); - this.target.store('mask', this); - this.setOptions(options); - this.render(); - this.inject(); - }, - - render: function(){ - this.element = new Element('div', { - 'class': this.options['class'], - id: this.options.id || 'mask-' + String.uniqueID(), - styles: Object.merge({}, this.options.style, { - display: 'none' - }), - events: { - click: function(event){ - this.fireEvent('click', event); - if (this.options.hideOnClick) this.hide(); - }.bind(this) - } - }); - - this.hidden = true; - }, - - toElement: function(){ - return this.element; - }, - - inject: function(target, where){ - where = where || (this.options.inject ? this.options.inject.where : '') || this.target == document.body ? 'inside' : 'after'; - target = target || (this.options.inject && this.options.inject.target) || this.target; - - this.element.inject(target, where); - - if (this.options.useIframeShim){ - this.shim = new IframeShim(this.element, this.options.iframeShimOptions); - - this.addEvents({ - show: this.shim.show.bind(this.shim), - hide: this.shim.hide.bind(this.shim), - destroy: this.shim.destroy.bind(this.shim) - }); - } - }, - - position: function(){ - this.resize(this.options.width, this.options.height); - - this.element.position({ - relativeTo: this.target, - position: 'topLeft', - ignoreMargins: !this.options.maskMargins, - ignoreScroll: this.target == document.body - }); - - return this; - }, - - resize: function(x, y){ - var opt = { - styles: ['padding', 'border'] - }; - if (this.options.maskMargins) opt.styles.push('margin'); - - var dim = this.target.getComputedSize(opt); - if (this.target == document.body){ - this.element.setStyles({width: 0, height: 0}); - var win = window.getScrollSize(); - if (dim.totalHeight < win.y) dim.totalHeight = win.y; - if (dim.totalWidth < win.x) dim.totalWidth = win.x; - } - this.element.setStyles({ - width: Array.pick([x, dim.totalWidth, dim.x]), - height: Array.pick([y, dim.totalHeight, dim.y]) - }); - - return this; - }, - - show: function(){ - if (!this.hidden) return this; - - window.addEvent('resize', this.position); - this.position(); - this.showMask.apply(this, arguments); - - return this; - }, - - showMask: function(){ - this.element.setStyle('display', 'block'); - this.hidden = false; - this.fireEvent('show'); - }, - - hide: function(){ - if (this.hidden) return this; - - window.removeEvent('resize', this.position); - this.hideMask.apply(this, arguments); - if (this.options.destroyOnHide) return this.destroy(); - - return this; - }, - - hideMask: function(){ - this.element.setStyle('display', 'none'); - this.hidden = true; - this.fireEvent('hide'); - }, - - toggle: function(){ - this[this.hidden ? 'show' : 'hide'](); - }, - - destroy: function(){ - this.hide(); - this.element.destroy(); - this.fireEvent('destroy'); - this.target.eliminate('mask'); - } - -}); - -Element.Properties.mask = { - - set: function(options){ - var mask = this.retrieve('mask'); - if (mask) mask.destroy(); - return this.eliminate('mask').store('mask:options', options); - }, - - get: function(){ - var mask = this.retrieve('mask'); - if (!mask){ - mask = new Mask(this, this.retrieve('mask:options')); - this.store('mask', mask); - } - return mask; - } - -}; - -Element.implement({ - - mask: function(options){ - if (options) this.set('mask', options); - this.get('mask').show(); - return this; - }, - - unmask: function(){ - this.get('mask').hide(); - return this; - } - -}); - - -/* ---- - -script: Spinner.js - -name: Spinner - -description: Adds a semi-transparent overlay over a dom element with a spinnin ajax icon. - -license: MIT-style license - -authors: - - Aaron Newton - -requires: - - Core/Fx.Tween - - Core/Request - - /Class.refactor - - /Mask - -provides: [Spinner] - -... -*/ - -var Spinner = new Class({ - - Extends: Mask, - - Implements: Chain, - - options: {/* - message: false,*/ - 'class': 'spinner', - containerPosition: {}, - content: { - 'class': 'spinner-content' - }, - messageContainer: { - 'class': 'spinner-msg' - }, - img: { - 'class': 'spinner-img' - }, - fxOptions: { - link: 'chain' - } - }, - - initialize: function(target, options){ - this.target = document.id(target) || document.id(document.body); - this.target.store('spinner', this); - this.setOptions(options); - this.render(); - this.inject(); - - // Add this to events for when noFx is true; parent methods handle hide/show. - var deactivate = function(){ this.active = false; }.bind(this); - this.addEvents({ - hide: deactivate, - show: deactivate - }); - }, - - render: function(){ - this.parent(); - - this.element.set('id', this.options.id || 'spinner-' + String.uniqueID()); - - this.content = document.id(this.options.content) || new Element('div', this.options.content); - this.content.inject(this.element); - - if (this.options.message){ - this.msg = document.id(this.options.message) || new Element('p', this.options.messageContainer).appendText(this.options.message); - this.msg.inject(this.content); - } - - if (this.options.img){ - this.img = document.id(this.options.img) || new Element('div', this.options.img); - this.img.inject(this.content); - } - - this.element.set('tween', this.options.fxOptions); - }, - - show: function(noFx){ - if (this.active) return this.chain(this.show.bind(this)); - if (!this.hidden){ - this.callChain.delay(20, this); - return this; - } - - this.active = true; - - return this.parent(noFx); - }, - - showMask: function(noFx){ - var pos = function(){ - this.content.position(Object.merge({ - relativeTo: this.element - }, this.options.containerPosition)); - }.bind(this); - - if (noFx){ - this.parent(); - pos(); - } else { - if (!this.options.style.opacity) this.options.style.opacity = this.element.getStyle('opacity').toFloat(); - this.element.setStyles({ - display: 'block', - opacity: 0 - }).tween('opacity', this.options.style.opacity); - pos(); - this.hidden = false; - this.fireEvent('show'); - this.callChain(); - } - }, - - hide: function(noFx){ - if (this.active) return this.chain(this.hide.bind(this)); - if (this.hidden){ - this.callChain.delay(20, this); - return this; - } - this.active = true; - return this.parent(noFx); - }, - - hideMask: function(noFx){ - if (noFx) return this.parent(); - this.element.tween('opacity', 0).get('tween').chain(function(){ - this.element.setStyle('display', 'none'); - this.hidden = true; - this.fireEvent('hide'); - this.callChain(); - }.bind(this)); - }, - - destroy: function(){ - this.content.destroy(); - this.parent(); - this.target.eliminate('spinner'); - } - -}); - -Request = Class.refactor(Request, { - - options: { - useSpinner: false, - spinnerOptions: {}, - spinnerTarget: false - }, - - initialize: function(options){ - this._send = this.send; - this.send = function(options){ - var spinner = this.getSpinner(); - if (spinner) spinner.chain(this._send.pass(options, this)).show(); - else this._send(options); - return this; - }; - this.previous(options); - }, - - getSpinner: function(){ - if (!this.spinner){ - var update = document.id(this.options.spinnerTarget) || document.id(this.options.update); - if (this.options.useSpinner && update){ - update.set('spinner', this.options.spinnerOptions); - var spinner = this.spinner = update.get('spinner'); - ['complete', 'exception', 'cancel'].each(function(event){ - this.addEvent(event, spinner.hide.bind(spinner)); - }, this); - } - } - return this.spinner; - } - -}); - -Element.Properties.spinner = { - - set: function(options){ - var spinner = this.retrieve('spinner'); - if (spinner) spinner.destroy(); - return this.eliminate('spinner').store('spinner:options', options); - }, - - get: function(){ - var spinner = this.retrieve('spinner'); - if (!spinner){ - spinner = new Spinner(this, this.retrieve('spinner:options')); - this.store('spinner', spinner); - } - return spinner; - } - -}; - -Element.implement({ - - spin: function(options){ - if (options) this.set('spinner', options); - this.get('spinner').show(); - return this; - }, - - unspin: function(){ - this.get('spinner').hide(); - return this; - } - -}); - diff --git a/couchpotato/static/scripts/library/prefix_free.js b/couchpotato/static/scripts/library/prefix_free.js index 96ed40e4..1ca634ee 100644 --- a/couchpotato/static/scripts/library/prefix_free.js +++ b/couchpotato/static/scripts/library/prefix_free.js @@ -1,2 +1,419 @@ -// StyleFix 1.0.1 & PrefixFree 1.0.4 / by Lea Verou / MIT license -(function(){function b(a,b){return[].slice.call((b||document).querySelectorAll(a))}if(!window.addEventListener)return;var a=window.StyleFix={link:function(b){try{if(b.rel!=="stylesheet"||!b.sheet.cssRules||b.hasAttribute("data-noprefix"))return}catch(c){return}var d=b.href||b.getAttribute("data-href"),e=d.replace(/[^\/]+$/,""),f=b.parentNode,g=new XMLHttpRequest;g.open("GET",d),g.onreadystatechange=function(){if(g.readyState===4){var c=g.responseText;if(c&&b.parentNode){c=a.fix(c,!0,b),e&&(c=c.replace(/url\((?:'|")?(.+?)(?:'|")?\)/gi,function(a,b){return/^([a-z]{3,10}:|\/|#)/i.test(b)?a:'url("'+e+b+'")'}),c=c.replace(RegExp("\\b(behavior:\\s*?url\\('?\"?)"+e,"gi"),"$1"));var d=document.createElement("style");d.textContent=c,d.media=b.media,d.disabled=b.disabled,d.setAttribute("data-href",b.getAttribute("href")),f.insertBefore(d,b),f.removeChild(b)}}},g.send(null),b.setAttribute("data-inprogress","")},styleElement:function(b){var c=b.disabled;b.textContent=a.fix(b.textContent,!0,b),b.disabled=c},styleAttribute:function(b){var c=b.getAttribute("style");c=a.fix(c,!1,b),b.setAttribute("style",c)},process:function(){b('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link),b("style").forEach(StyleFix.styleElement),b("[style]").forEach(StyleFix.styleAttribute)},register:function(b,c){(a.fixers=a.fixers||[]).splice(c===undefined?a.fixers.length:c,0,b)},fix:function(b,c){for(var d=0;d3){d.pop();var f=d.join("-");h(f)&&b.indexOf(f)===-1&&b.push(f)}}},h=function(a){return StyleFix.camelCase(a)in f};if(e.length>0)for(var i=0;i 3) { + parts.pop(); + + var shorthand = parts.join('-'); + + if(supported(shorthand) && properties.indexOf(shorthand) === -1) { + properties.push(shorthand); + } + } + } + }, + supported = function(property) { + return StyleFix.camelCase(property) in dummy; + } + + // Some browsers have numerical indices for the properties, some don't + if(style.length > 0) { + for(var i=0; i> 1) : o.left+mid) + 'px', + top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : o.top+mid) + 'px' + }); + } + + el.setAttribute('aria-role', 'progressbar'); + self.lines(el, self.opts); + + if (!useCssAnimations) { + // No CSS animation support, use setTimeout() instead + var i = 0; + var fps = o.fps; + var f = fps/o.speed; + var ostep = (1-o.opacity)/(f*o.trail / 100); + var astep = f/o.lines; + + !function anim() { + i++; + for (var s=o.lines; s; s--) { + var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity); + self.opacity(el, o.lines-s, alpha, o); + } + self.timeout = self.el && setTimeout(anim, ~~(1000/fps)); + }(); + } + return self; + }, + stop: function() { + var el = this.el; + if (el) { + clearTimeout(this.timeout); + if (el.parentNode) el.parentNode.removeChild(el); + this.el = undefined; + } + return this; + }, + lines: function(el, o) { + var i = 0; + var seg; + + function fill(color, shadow) { + return css(createEl(), { + position: 'absolute', + width: (o.length+o.width) + 'px', + height: o.width + 'px', + background: color, + boxShadow: shadow, + transformOrigin: 'left', + transform: 'rotate(' + ~~(360/o.lines*i) + 'deg) translate(' + o.radius+'px' +',0)', + borderRadius: (o.width>>1) + 'px' + }); + } + for (; i < o.lines; i++) { + seg = css(createEl(), { + position: 'absolute', + top: 1+~(o.width/2) + 'px', + transform: o.hwaccel ? 'translate3d(0,0,0)' : '', + opacity: o.opacity, + animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite' + }); + if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'})); + ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)'))); + } + return el; + }, + opacity: function(el, i, val) { + if (i < el.childNodes.length) el.childNodes[i].style.opacity = val; + } + }; + + ///////////////////////////////////////////////////////////////////////// + // VML rendering for IE + ///////////////////////////////////////////////////////////////////////// + + /** + * Check and init VML support + */ + !function() { + var s = css(createEl('group'), {behavior: 'url(#default#VML)'}); + var i; + + if (!vendor(s, 'transform') && s.adj) { + + // VML support detected. Insert CSS rules ... + for (i=4; i--;) sheet.addRule(['group', 'roundrect', 'fill', 'stroke'][i], 'behavior:url(#default#VML)'); + + Spinner.prototype.lines = function(el, o) { + var r = o.length+o.width; + var s = 2*r; + + function grp() { + return css(createEl('group', {coordsize: s +' '+s, coordorigin: -r +' '+-r}), {width: s, height: s}); + } + + var margin = -(o.width+o.length)*2+'px'; + var g = css(grp(), {position: 'absolute', top: margin, left: margin}); + + var i; + + function seg(i, dx, filter) { + ins(g, + ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}), + ins(css(createEl('roundrect', {arcsize: 1}), { + width: r, + height: o.width, + left: o.radius, + top: -o.width>>1, + filter: filter + }), + createEl('fill', {color: o.color, opacity: o.opacity}), + createEl('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change + ) + ) + ); + } + + if (o.shadow) { + for (i = 1; i <= o.lines; i++) { + seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)'); + } + } + for (i = 1; i <= o.lines; i++) seg(i); + return ins(el, g); + }; + Spinner.prototype.opacity = function(el, i, val, o) { + var c = el.firstChild; + o = o.shadow && o.lines || 0; + if (c && i+o < c.childNodes.length) { + c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild; + if (c) c.opacity = val; + } + }; + } + else { + useCssAnimations = vendor(s, 'animation'); + } + }(); + + window.Spinner = Spinner; + +})(window, document); diff --git a/couchpotato/static/scripts/library/templated.js b/couchpotato/static/scripts/library/templated.js deleted file mode 100644 index 6ba2343e..00000000 --- a/couchpotato/static/scripts/library/templated.js +++ /dev/null @@ -1,411 +0,0 @@ -/* ---- -description: Templated is a MooTools mixin class which creates elements for classes using a string-based HTML template which may included embedded attach points and events. - -license: MIT-style - -authors: -- David Walsh - -requires: -- core/1.3: '*' - -provides: [Templated] - -... -*/ - -// Create scope limiter -(function(scope) { - - // Create some vars for strings - // This will save bytes when compressed - var strData = "data", - strWidget = "widget", - strAttach = "attach", - dataWidgetType = strData + "-" + strWidget + "-type", - dataWidgetProps = strData + "-" + strWidget + "-props", - dataWidgetAttachPoint = strData + "-" + strWidget + "-" + strAttach + "-point", - dataWidgetAttachEvent = strData + "-" + strWidget + "-" + strAttach + "-event", - dataWidgetized = strData + "-widgetized"; - - // Templated is a mixin for UI widgets - scope.Templated = new Class({ - - // The usual options object - options: { - // The default template - template: "
", - - // The URL to get the template from *instead* of the string - templateUrl: "", - - // A node reference for where this UI widget will be placed...in reference to - element: null, - - // Should this widget be parsed for sub-widgets? - widgetsInTemplate: true, - - // Property mappings (should be an object) - // These override defaults - propertyMappings: null, - - // Default property mappings - // Thse properties on the element will be moved to the respective nodes within the template - defaultPropertyMappings: null, - - // Should messages be debug to the console - debugMode: true - }, - - // Create placeholders for attached points and events - _attachPoints: [], - _attachEvents: [], - - // Parse - parse: function() { - - // Get shortcuts to the options and element - var options = this.options, - nodeRef = options.element = options.element || new Element("div").inject(document.body); - - // *IF* a templateUrl is specified, can't do anything until template is loaded - // Defer parsing until we've got it - if(options.templateUrl && Templated.templates && !Templated.templates[options.templateUrl]) { - this.debug("[Templated:parse] Need to load template from URL: " + options.templateUrl); - this.getTemplate(); - return false; - } - - // If already data-widgetized...gtfo - if(nodeRef.retrieve("widget")) { - this.debug("[Templated:parse] Node already widgetized, leaving ", nodeRef); - return nodeRef.domNode; - } - - // Mix noderef properties with options - options.defaultPropertyMappings = options.defaultPropertyMappings || { // THESE OVERRIDE CLASSES IN THE TEMPLATE!!!! - "id": "domNode", - "style": "domNode", - "class": "domNode" - }; - Object.merge(options, this.getNodeProps(nodeRef)); - - // postMixInProperties runs after options have been mixed with defaults but before - // any templating is done - this.postMixInProperties(); - - // Build rendering - creates the actual nodes, attachpoints, and attachevents - this.buildRendering(); - - // Fire the "postCreate" method, which runs after nodes are created *but* before the nodes are rendered to the page - this.postCreate(); - - // Cleanup creation - this.cleanupCreation(); - - // "Startup": The widget is in the DOM and the widget is ready to go - this.startup(); - - // Return the domNode - this.debug("[Templated:parse] At the end of parse, this is: ", this); - return this.domNode; - }, - - // Creates build rendering - buildRendering: function() { - // Get shortcuts to the options and element - var options = this.options, nodeRef = options.element; - - // Do string substitution on the template - var template = this.template = options.template.substitute(options || {}); - - // Create the DOM node within a DIV that's not rendered to the page - var bitchNode = this.bitchNode = new Element("div", { html: template.trim() }), - domNode = this.domNode = document.id(bitchNode.childNodes[0]); - - // Look for subwidgets if told to... - if(options.widgetsInTemplate) { - this.debug("[Templated:parse] Looking for subwidgets under domNode", domNode); - this.makeSubWidgets(this.domNode); - } - - // Create the attachpoints for me, then my kiddies - this.makeAttachPoints(domNode); - if(options.widgetsInTemplate) domNode.getElements("[" + dataWidgetAttachPoint + "]").each(this.makeAttachPoints, this); - this.debug("[Templated:parse] Creating attachpoints", this._attachPoints); - - // Create the attachevents for me, then my kiddies - this.makeAttachEvents(domNode); - if(options.widgetsInTemplate) domNode.getElements("[" + dataWidgetAttachEvent + "]").each(this.makeAttachEvents, this); - this.debug("Creating attachevents", this._attachEvents); - - // Map properties to nodes within the template - // Mix the custom mappings with the default - // This needs to happen after attachpoints - var mappings = options.propertyMappings ? Object.merge(options.defaultPropertyMappings, options.propertyMappings) : options.defaultPropertyMappings; - Object.each(mappings, function(value, key) { - // Ignore the value if not present in the object - if(!this[value]) return; - // Assign the value to the key - var currentProp = nodeRef.get(key); - if(currentProp != "") this[value].set(key, currentProp); - }.bind(this)); - - // If this widget has a "containerNode", grab it's childNodes *or* inject innerHTML - if(this.containerNode) { - var kids = nodeRef.childNodes; - kids.length ? $$(kids).inject(this.containerNode) : this.containerNode.set("html", nodeRef.get("html")); - } - }, - - // "postMixInProperties" -- Fired after options have been mixed in - postMixInProperties: function() { - this.debug("[Templated:postMixInProperties] postMixInProperties!"); - }, - - // "PostCreate" -- Fired after nodes are created, attachpoints and events are found - postCreate: function() { - this.debug("[Templated:postCreate] postCreate!"); - }, - - // "CleanupCreation" -- Removes the old element, destroys bitch node - cleanupCreation: function() { - // Get hold of the dom node and bitch nodes - var domNode = this.domNode, bitchNode = this.bitchNode, nodeRef = this.options.element; - - // Put the domNode where it should go and destroy the node reference - domNode.replaces(nodeRef); - nodeRef.destroy(); - - // Mark as data-widgetized and store the widget within data - domNode.set(dataWidgetized, true); - domNode.store("widget", this); - - // Remove the bitch node - bitchNode.destroy(); - }, - - // "StartUp" -- Fired when node is in place - startup: function(){ - this.debug("[Templated:startup] startup!"); - }, - - // Focus on focus node, if present - focus: function() { - var node = this.focusNode; - node && node.focus(); - }, - - // Create subwidgets from this - makeSubWidgets: function(domNode) { - if(!domNode) domNode = this.domNode; - domNode.getElements("["+ dataWidgetType +"]:not([" + dataWidgetized + "])").each(function(node){ - // Store the subwidget's attachpoints, attachevents, class type, and properties - var points = node.get(dataWidgetAttachPoint), - events = node.get(dataWidgetAttachEvent), - widgetProps = this.getNodeProps(node), - klass = node.get(dataWidgetType).trim(); - - // Create the widget - if(scope[klass]) { - var widget = new scope[klass](Object.merge(widgetProps, { element: node })); - this.debug("[parse:makeSubWidgets] Creating child widget: ", klass, widget); - // Get access to its dom node - widgetDomNode = widget.domNode; - // Add attachments back to the widget - points && widgetDomNode.set(dataWidgetAttachPoint, points.trim()); - events && widgetDomNode.set(dataWidgetAttachEvent, events.trim()); - } - }, this); - }, - - // Makes attachpoints - makeAttachPoints: function(node) { - var points = node.get(dataWidgetAttachPoint); - if(points) { - points.trim().split(",").each(function(attach) { - attach = attach.trim(); - this[attach] = node.retrieve("widget") || node; - this.debug("[Templated:makeAttachPoints] " + attach, node); - this._attachPoints.push({ node: node, name: attach }); - }, this); - node.set(dataWidgetAttachPoint, ""); - node.set("data-widgetized-attach-point", points); - } - }, - - // Makes attachevents - makeAttachEvents: function(node) { - // Temporarily store this widget's events so they may be added to this.domNode later - var events = node.get(dataWidgetAttachEvent); - // If there are events.... - if(events) { - // For every event found.... - events.trim().split(",").each(function(event) { - // Trim the event - event = event.trim(); - // Split the event:method pair - var eventFn = event.split(":"); - // Trim and rename each piece - var nativeEvent = eventFn[0].trim(), - classEvent = eventFn[1].trim(); - this.debug("[Templated:makeAttachEvents] " + nativeEvent + " / " + classEvent,node); - - // If the method isn't found on this, create a stub for it' - if(!this[classEvent]) { - this.debug("cant find ", classEvent, " in: ", this, " creating sub for it"); - this[classEvent] = function(){}; - } - - // Bind "this" to the event - var ev = this[classEvent].bind(this); - - // Add the event to the domNode - node.addEvent(nativeEvent, ev); - - // Store the event - this._attachEvents.push({ type: nativeEvent, event: ev, node: node }); - }, this); - - // Remove the event from its former place and add to -ized data item - var set = { - "data-widgetized-attach-event": events - }; - set[dataWidgetAttachEvent] = ""; - node.set(set); - //node.set("data-widget-attach-event",""); - //node.set("data-widgetized-attach-event",events); - } - }, - - // Destroy: removes node events - destroy: function() { - // Get reference to domNode - var domNode = this.domNode, events = this._attachEvents, points = this._attachPoints; - - // Clear out children - domNode.getElements("[" + dataWidgetized + "]").each(function(widget) { - widget.destroy(); - }); - - // Remove events - if(events.length) { - events.each(function(event) { - if(event.node) event.node.removeEvents(); - }); - } - // Remove node connections - if(points.length) { - points.each(function(point) { - if(point.name != "domNode") this[point.name] = null; - }, this); - } - // Destroy the dom node and its children, fin - domNode.store("widget", null).destroy(); - }, - - // Gets the in-node properties for a widget - getNodeProps: function(node) { - var props = node.get(dataWidgetProps), - widgetProps = {}; - // Create the widget - if(props) { - // Not using JSON.parse because it's too restricting, especially with quotes - var json = "{" + props.trim() + "}"; - if(JSON && JSON.decode) { // MooTools - widgetProps = JSON.decode(json); - } - else { // Native - eval("widgetProps = " + json); - } - } - return widgetProps; - }, - - debug: function(one, two, three, four) { - if(this.options.debugMode && console && console.log) { - console.log("[" + (this.domNode ? this.domNode.id : "") + "] ", one, two || "", three || "", four || ""); - } - } - }); - - // If Request is available.... - if(Request) { - Templated.templates = {}; - // Return the template - Templated.implement({ - // Method to return cached template or retrieve new one synchronously - getTemplate: function() { - /* - var url = this.options.templateUrl; - // Try to return cached first - if(Templated.templates[url]) { - return Templated.templates[url]; - } - else { - // Send a new request - return new Request({ - url: url, - async: false, // Used to ensure that necessary templates are there - onSuccess: function(template) { - this.options.template = template; - Templated.templates[url] = template; - this.parse(); - }.bind(this) - }).send(); - } - */ - - var url = this.options.templateUrl; - // Try to return cached first - if(!Templated.templates[url]) { - // Send a new request - return new Request({ - url: url, - async: false, // Used to ensure that necessary templates are there - onSuccess: function(template) { - this.options.template = template; - Templated.templates[url] = template; - this.parse(); - }.bind(this), - onFailure: function() { - Templated.templates[url] = Templated.prototype.options.template; - } - }).send(); - } - return Templated.templates[url]; - } - }); - } - - // Allow for parsing of an element and its children - Element.implement({ - parse: function() { - - var elFn = function(element) { - // Get the widget type - var klass = element.get(dataWidgetType); - // If the class exists.... - if(klass && scope[klass]) { - // Create the new class instance - new scope[klass]({ element: element }); - } - else { - window.console && console.log && console.log("klass does not exist! ", klass); - } - }; - - // Grab this and all nodes which are not already widgetized - elFn(this); - $$(this.getElements("["+ dataWidgetType +"]:not(" + dataWidgetized + ")")).each(elFn); - } - }); - - // Get widget from id - document.widget = function(idOrNode) { - return document.id(idOrNode).retrieve("widget") || null; - }; - - -})(this); // Scope limiter \ No newline at end of file diff --git a/couchpotato/static/scripts/page.js b/couchpotato/static/scripts/page.js index af77cfe5..589fa3ed 100644 --- a/couchpotato/static/scripts/page.js +++ b/couchpotato/static/scripts/page.js @@ -46,7 +46,7 @@ var PageBase = new Class({ self.fireEvent('error'); } }, - + openUrl: function(url){ if(History.getPath() != url) History.push(url); @@ -59,15 +59,15 @@ var PageBase = new Class({ getName: function(){ return this.name }, - + show: function(){ this.el.addClass('active'); }, - + hide: function(){ this.el.removeClass('active'); }, - + toElement: function(){ return this.el } diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js index 20ebcb22..ad0dd5b9 100644 --- a/couchpotato/static/scripts/page/about.js +++ b/couchpotato/static/scripts/page/about.js @@ -38,6 +38,40 @@ var AboutSettingTab = new Class({ today = new Date(), one_day = 1000*60*60*24; + self.settings.createGroup({ + 'label': 'About This CouchPotato', + 'name': 'variables' + }).inject(self.content).adopt( + new Element('dl.info').adopt( + new Element('dt[text=Version]'), + self.version_text = new Element('dd.version', { + 'text': 'Getting version...', + 'events': { + 'click': App.checkForUpdate.bind(App, function(json){ + self.fillVersion(json) + }), + 'mouseenter': function(){ + this.set('text', 'Check for updates') + }, + 'mouseleave': function(){ + self.fillVersion(Updater.getInfo()) + } + } + }), + new Element('dt[text=ID]'), + new Element('dd', {'text': App.getOption('pid')}), + new Element('dt[text=Directories]'), + new Element('dd', {'text': App.getOption('app_dir')}), + new Element('dd', {'text': App.getOption('data_dir')}), + new Element('dt[text=Startup Args]'), + new Element('dd', {'html': App.getOption('args')}), + new Element('dd', {'html': App.getOption('options')}) + ) + ); + + if(!self.fillVersion(Updater.getInfo())) + Updater.addEvent('loaded', self.fillVersion.bind(self)) + self.settings.createGroup({ 'name': 'Help Support CouchPotato' }).inject(self.content).adopt( @@ -78,56 +112,6 @@ var AboutSettingTab = new Class({ }) ); - - self.settings.createGroup({ - 'label': 'About This CouchPotato', - 'name': 'variables' - }).inject(self.content).adopt( - new Element('dl.info').adopt( - new Element('dt[text=Version]'), - self.version_text = new Element('dd.version', { - 'text': 'Getting version...', - 'events': { - 'click': self.checkForUpdate.bind(self), - 'mouseenter': function(){ - this.set('text', 'Check for updates') - }, - 'mouseleave': function(){ - self.fillVersion(Updater.getInfo()) - } - } - }), - new Element('dt[text=Directories]'), - new Element('dd', {'text': App.getOption('app_dir')}), - new Element('dd', {'text': App.getOption('data_dir')}), - new Element('dt[text=Startup Args]'), - new Element('dd', {'html': App.getOption('args')}), - new Element('dd', {'html': App.getOption('options')}) - ) - ); - - if(!self.fillVersion(Updater.getInfo())) - Updater.addEvent('loaded', self.fillVersion.bind(self)) - - self.settings.createGroup({ - 'name': 'actions' - }).inject(self.content).adopt( - new Element('div').adopt( - new Element('a.button.red', { - 'text': 'Shutdown', - 'events': { - 'click': App.shutdown.bind(App) - } - }), - new Element('a.button.orange', { - 'text': 'Restart', - 'events': { - 'click': App.restart.bind(App) - } - }) - ) - ); - }, fillVersion: function(json){ @@ -135,17 +119,6 @@ var AboutSettingTab = new Class({ var self = this; var date = new Date(json.version.date * 1000); self.version_text.set('text', json.version.hash + ' ('+date.toUTCString()+')'); - }, - - checkForUpdate: function(){ - var self = this; - - Updater.check(function(json){ - self.fillVersion(json) - }) - - App.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); - App.checkAvailable(3000); } }); diff --git a/couchpotato/static/scripts/page/manage.js b/couchpotato/static/scripts/page/manage.js index 0ed3dfc8..74dba986 100644 --- a/couchpotato/static/scripts/page/manage.js +++ b/couchpotato/static/scripts/page/manage.js @@ -9,27 +9,42 @@ Page.Manage = new Class({ var self = this; if(!self.list){ - self.refresh_button = new Element('a.icon.refresh', { - 'text': 'Refresh', + self.refresh_button = new Element('a', { + 'title': 'Rescan your library for new movies', + 'text': 'Full library refresh', 'events':{ - 'click': self.refresh.bind(self) + 'click': self.refresh.bind(self, true) } - }).inject(self.el); + }); + + self.refresh_quick = new Element('a', { + 'title': 'Just scan for recently changed', + 'text': 'Quick library scan', + 'events':{ + 'click': self.refresh.bind(self, false) + } + }); self.list = new MovieList({ + 'identifier': 'manage', 'status': 'done', - 'navigation': true, - 'actions': MovieActions + 'actions': MovieActions, + 'menu': [self.refresh_button, self.refresh_quick] }); $(self.list).inject(self.el); } }, - refresh: function(){ + refresh: function(full){ var self = this; + p(full) - Api.request('manage.update') + Api.request('manage.update', { + 'data': { + 'full': full ? 1 : null + } + }) } diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index c4dede3d..94643cc7 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -8,6 +8,21 @@ Page.Settings = new Class({ tabs: {}, current: 'about', + has_tab: false, + + initialize: function(options){ + var self = this; + self.parent(options); + + // Add to more menu + if(self.name == 'settings') + App.getBlock('more').addLink(new Element('a', { + 'href': App.createUrl(self.name), + 'text': self.name.capitalize(), + 'title': self.title + }), 'top') + + }, open: function(action, params){ var self = this; @@ -40,8 +55,26 @@ Page.Settings = new Class({ var c = 'active'; var t = self.tabs[tab_name] || self.tabs[self.action] || self.tabs.general; + + // Subtab + var subtab = null + Object.each(self.params, function(param, subtab_name){ + subtab = subtab_name; + }) + + self.el.getElements('li.'+c+' , .tab_content.'+c).each(function(active){ + active.removeClass(c); + }); + + if (t.subtabs[subtab]){ + t.tab[a](c); + t.subtabs[subtab].tab[a](c); + t.subtabs[subtab].content[a](c); + } + else { t.tab[a](c); t.content[a](c); + } return t }, @@ -107,10 +140,20 @@ Page.Settings = new Class({ new Form.Check(self.advanced_toggle); // Add content to tabs + var options = []; Object.each(json.options, function(section, section_name){ + section['section_name'] = section_name; + options.include(section); + }) + + options.sort(function(a, b){ + return (a.order || 100) - (b.order || 100) + }).each(function(section){ + var section_name = section.section_name; // Add groups to content section.groups.sortBy('order').each(function(group){ + if(group.hidden) return; if(self.wizard_only && !group.wizard) return; @@ -118,17 +161,28 @@ Page.Settings = new Class({ // Create tab if(!self.tabs[group.tab] || !self.tabs[group.tab].groups) self.createTab(group.tab, {}); + var content_container = self.tabs[group.tab].content + + // Create subtab + if(group.subtab){ + if (!self.tabs[group.tab].subtabs[group.subtab]) + self.createSubTab(group.subtab, {}, self.tabs[group.tab], group.tab); + var content_container = self.tabs[group.tab].subtabs[group.subtab].content + } // Create the group if(!self.tabs[group.tab].groups[group.name]){ var group_el = self.createGroup(group) - .inject(self.tabs[group.tab].content) + .inject(content_container) .addClass('section_'+section_name); self.tabs[group.tab].groups[group.name] = group_el } // Add options to group - group.options.sortBy('order').each(function(option){ + group.options.sort(function(a, b){ + return (a.order || 100) - (b.order || 100) + }).each(function(option){ + if(option.hidden) return; var class_name = (option.type || 'string').capitalize(); var input = new Option[class_name](section_name, option.name, self.getValue(section_name, option.name), option); input.inject(self.tabs[group.tab].groups[group.name]); @@ -164,6 +218,7 @@ Page.Settings = new Class({ self.tabs[tab_name] = Object.merge(self.tabs[tab_name], { 'tab': tab_el, + 'subtabs': {}, 'content': new Element('div.tab_content.tab_'+tab_name).inject(self.containers), 'groups': {} }) @@ -172,11 +227,43 @@ Page.Settings = new Class({ }, + createSubTab: function(tab_name, tab, parent_tab, parent_tab_name){ + var self = this; + + if(parent_tab.subtabs[tab_name]) + return parent_tab.subtabs[tab_name] + + if(!parent_tab.subtabs_el) + parent_tab.subtabs_el = new Element('ul.subtabs').inject(parent_tab.tab); + + var label = (tab.label || tab.name || tab_name).capitalize() + var tab_el = new Element('li.t_'+tab_name).adopt( + new Element('a', { + 'href': App.createUrl(self.name+'/'+parent_tab_name+'/'+tab_name), + 'text': label + }).adopt() + ).inject(parent_tab.subtabs_el); + + if(!parent_tab.subtabs[tab_name]) + parent_tab.subtabs[tab_name] = { + 'label': label + } + + parent_tab.subtabs[tab_name] = Object.merge(parent_tab.subtabs[tab_name], { + 'tab': tab_el, + 'content': new Element('div.tab_content.tab_'+tab_name).inject(self.containers), + 'groups': {} + }); + + return parent_tab.subtabs[tab_name] + + }, + createGroup: function(group){ var self = this; var group_el = new Element('fieldset', { - 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + 'class': (group.advanced ? 'inlineLabels advanced' : 'inlineLabels') + ' group_' + (group.name || '') + ' subtab_' + (group.subtab || '') }).adopt( new Element('h2', { 'text': (group.label || group.name).capitalize() @@ -541,7 +628,7 @@ Option.Directory = new Class({ self.dir_list = new Element('ul', { 'events': { 'click:relay(li)': function(e, el){ - (e).stop(); + (e).preventDefault(); self.selectDirectory(el.get('data-value')) }, 'mousewheel': function(e){ @@ -591,7 +678,7 @@ Option.Directory = new Class({ hideBrowser: function(e, save){ var self = this; - (e).stop(); + (e).preventDefault(); if(save) self.save() @@ -1154,7 +1241,7 @@ Option.Combined = new Class({ deleteCombinedItem: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); var item = e.target.getParent(); diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index c2ac6c08..8d1dd931 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -12,8 +12,8 @@ Page.Wanted = new Class({ // Wanted movies self.wanted = new MovieList({ + 'identifier': 'wanted', 'status': 'active', - 'navigation': true, 'actions': MovieActions }); $(self.wanted).inject(self.el); @@ -49,7 +49,7 @@ window.addEvent('domready', function(){ editMovie: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( @@ -75,7 +75,7 @@ window.addEvent('domready', function(){ }).inject(self.title_select); }); - Object.each(Quality.getActiveProfiles(), function(profile){ + Quality.getActiveProfiles().each(function(profile){ new Element('option', { 'value': profile.id ? profile.id : profile.data.id, 'text': profile.label ? profile.label : profile.data.label @@ -89,7 +89,7 @@ window.addEvent('domready', function(){ }, save: function(e){ - (e).stop(); + (e).preventDefault(); var self = this; Api.request('movie.edit', { @@ -129,7 +129,7 @@ window.addEvent('domready', function(){ doRefresh: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); Api.request('movie.refresh', { 'data': { @@ -160,7 +160,7 @@ window.addEvent('domready', function(){ showConfirm: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.delete_container){ self.delete_container = new Element('div.delete_container').adopt( @@ -188,13 +188,13 @@ window.addEvent('domready', function(){ hideConfirm: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); self.movie.slide('out'); }, del: function(e){ - (e).stop(); + (e).preventDefault(); var self = this; var movie = $(self.movie); @@ -253,7 +253,7 @@ window.addEvent('domready', function(){ showFiles: function(e){ var self = this; - (e).stop(); + (e).preventDefault(); if(!self.options_container){ self.options_container = new Element('div.options').adopt( diff --git a/couchpotato/static/style/api.css b/couchpotato/static/style/api.css new file mode 100644 index 00000000..c6354098 --- /dev/null +++ b/couchpotato/static/style/api.css @@ -0,0 +1,96 @@ +html { + font-size: 12px; + line-height: 1.5; + font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + font-size: 14px; +} + +* { + margin: 0; + padding: 0; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +h1, h2, h3, h4, h5 { + clear: both; + font-size: 14px; +} + +h1 { + font-size: 25px; + padding: 20px 40px; +} + +h2 { + font-size: 20px; +} + +pre { + background: #eee; + font-family: monospace; + margin: 0; + padding: 10px; + width: 100%; + display: block; + font-size: 12px; +} + +.api, .missing { + overflow: hidden; + border-bottom: 1px solid #eee; + padding: 40px; +} + .api:hover { + color: #000; + } + + .api .description { + color: #333; + padding: 0 0 5px; + } + + .api .params { + background: #fafafa; + width: 100%; + } + .api h3 { + clear: both; + float: left; + width: 100px; + } + + .api .params { + float: left; + width: 700px; + } + + .api .params td, .api .params th { + padding: 3px 5px; + border-bottom: 1px solid #eee; + } + .api .params tr:last-child td, .api .params tr:last-child th { + border: 0; + } + + .api .params .param { + vertical-align: top; + } + + .api .params .param th { + text-align: left; + width: 100px; + } + + .api .param .type { + font-style: italic; + margin-right: 10px; + width: 100px; + color: #666; + } + + .api .return { + float: left; + width: 700px; + } \ No newline at end of file diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 8446492c..a128ee32 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -1,10 +1,3 @@ -/* @override - http://localhost:5000/static/style/main.css - http://192.168.1.20:5000/static/style/main.css - http://127.0.0.1:5000/static/style/main.css - http://127.0.0.1:5000/v2/_api_/static/style/main.css -*/ - html { color: #fff; font-size: 12px; @@ -20,6 +13,7 @@ body { background: #4e5969; overflow-y: scroll; height: 100%; + text-align: justify; } body.noscroll { overflow: hidden; } @@ -27,6 +21,12 @@ body { background: transparent !important; } +* { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + pre { white-space: pre-wrap; word-wrap: break-word; @@ -78,7 +78,7 @@ a:hover { color: #f3f3f3; } .content { clear:both; - padding: 80px 10px 10px; + padding: 80px 0 10px; } .footer { @@ -100,9 +100,9 @@ a:hover { color: #f3f3f3; } right: 0; padding: 10px 10px 10px 40px; background: #f7f7f7 url('../images/toTop.gif') no-repeat 10px center; - border-radius: 5px 0 0 0; + border-radius: 5px 0 0 0; } - + form { padding:0; margin:0; @@ -111,7 +111,16 @@ form { body > .spinner, .mask{ background: rgba(0,0,0, 0.9); z-index: 100; + text-align: center; } + body > .mask { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + padding: 200px; + } .button { background: #5082bc url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAyCAYAAACd+7GKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpi/v//vwMTAwPDfzjBgMpFI/7hFSOT9Y8qRuF3JLoHAQIMAHYtMmRA+CugAAAAAElFTkSuQmCC") repeat-x; @@ -149,16 +158,17 @@ body > .spinner, .mask{ /*** Navigation ***/ .header { background: #4e5969; - padding:10px; - height: 60px; + padding: 10px 0; + height: 80px; position: fixed; - width: 99%; - z-index: 2; - box-shadow: 0 0 5px rgba(0,0,0,0.1); + margin: 0; + width: 100%; + z-index: 5; + box-shadow: 0 20px 30px -30px rgba(0,0,0,0.05); transition: box-shadow .4s cubic-bezier(0.9,0,0.1,1); } .header.with_shadow { - box-shadow: 0 0 50px rgba(0,0,0,0.3); + box-shadow: 0 20px 30px -30px rgba(0,0,0,0.3); } .header > div { @@ -168,7 +178,8 @@ body > .spinner, .mask{ } .header .navigation { display: inline-block; - width: 75%; + vertical-align: middle; + width: 67.2%; } .header .navigation ul { margin: 0; @@ -188,30 +199,23 @@ body > .spinner, .mask{ padding: 15px; } .header .navigation li:first-child a { padding-left: 10px; } - .header .navigation li a.logLink { font-size: 13px; padding: 23px 20px 15px; } - .header .navigation li a#showConfig { - background: url('../../media/images/gear.png') no-repeat center; - height: 35px; - width: 10px; - } - .header .navigation li span { display: block; margin-top: 5px; } - + .header .navigation li.disabled { color: #e5e5e5; } - + .header .navigation li a:link, .header .navigation li a:visited { color: #fff; } - + .header .navigation li a:hover, .header .navigation li a:active { color: #b1d8dc; } - + .header .navigation .backtotop { opacity: 0; display: block; @@ -228,7 +232,75 @@ body > .spinner, .mask{ font-weight: normal; } .header:hover .navigation .backtotop { color: #fff; } - + + .header .more_menu { + margin-left: 12px; + } + .header .more_menu .wrapper { + width: 150px; + margin-left: -110px; + } + .header .more_menu .wrapper:before { + margin-left: -34px; + } + + .header .more_menu .red { color: red; } + .header .more_menu .orange { color: orange; } + + .badge { + position: absolute; + width: 14px; + height: 14px; + text-align: center; + line-height: 14px; + border-radius: 50%; + font-size: 8px; + margin: -5px 0 0 15px; + box-shadow: inset 0 1px 0 rgba(255,255,255,.6), 0 0 3px rgba(0,0,0,.7); + background: -webkit-gradient(linear, left bottom, left top, from(rgba(255,255,255,.3)), to(rgba(255,255,255,.1))); + background: -moz-linear-gradient(center bottom, rgba(255,255,255,.3) 0%, rgba(255,255,255,.1) 100%); + background-color: #1b79b8; + text-shadow: none; + } + + .header .notification_menu .wrapper { + width: 300px; + margin-left: -260px; + text-align: left; + } + + .header .notification_menu .wrapper:before { + left: 296px; + } + + .header .notification_menu ul { + max-height: 300px; + overflow: auto; + } + + .header .notification_menu > a { + background-position: center -209px; + } + + .header .notification_menu li > span { + padding: 5px; + display: block; + border-bottom: 1px solid rgba(0,0,0,0.2); + word-wrap: break-word; + } + .header .notification_menu li > span { color: #777; } + .header .notification_menu li:last-child > span { border: 0; } + .header .notification_menu li .added { + display: block; + font-size: 10px; + color: #aaa; + text-align: ; + } + + .header .notification_menu li .more { + text-align: center; + } + .header .message.update { text-align: center; position: relative; @@ -251,15 +323,14 @@ body > .spinner, .mask{ height: 16px; width: 16px; cursor: pointer; + background: url('../images/sprite.png') no-repeat -200px; } .check.highlighted { background-color: #424c59; } - .check.checked { - background-image: url('../images/checks.png'); - background-position: -2px 0; -} -.check input { - display: none !important; -} + .check.checked { background-position: -2px 0; } + .check.indeterminate { background-position: -1px -119px; } + .check input { + display: none !important; + } .select { cursor: pointer; @@ -271,28 +342,28 @@ body > .spinner, .mask{ display: inline-block; padding: 0 30px 0 20px; border-radius:30px; - + box-shadow: 0 1px 1px rgba(0,0,0,0.35), inset 0 1px 0px rgba(255,255,255,0.20); - - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, #406db8), color-stop(1, #5b9bd1) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, #5b9bd1 0%, #406db8 100% ); } - + .select .selection .selectionDisplay { display: inline-block; padding-right: 15px; border-right: 1px solid rgba(0,0,0,0.2); - + box-shadow: 1px 0 0 rgba(255,255,255,0.15); } @@ -301,17 +372,28 @@ body > .spinner, .mask{ overflow: hidden; font-weight: bold; } + + .select .list:before { + content: ' '; + height: 0; + position: absolute; + width: 0; + border: 6px solid transparent; + border-bottom-color: #282d34; + margin: -11px 0 0 70px; + } + .select .list { display: none; background: #282d34; border: 1px solid #1f242b; position: absolute; - margin: 25px 0 0 0; - box-shadow: 0 1px 2px rgba(0,0,0,0.4); + margin: 30px 0 0 0; + box-shadow: 0 20px 20px -10px rgba(0,0,0,0.4); border-radius:3px; z-index: 3; } - .select.active .list { + .select.active .list { display: block; } .select .list ul { @@ -329,29 +411,29 @@ body > .spinner, .mask{ background: rgba(255,255,255,0.1); border-color: transparent; } - + .select input { display: none; } .inlay { color: #fff; border: 0; border-radius:3px; - background: #282d34; + background-color: #282d34; box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25); } .inlay.light { - background: #47515f; + background-color: #47515f; outline: none; box-shadow: inset 0 1px 8px rgba(0,0,0,0.05), 0 1px 0px rgba(255,255,255,0.15); } - + .inlay:focus { - background: #3a4350; + background-color: #3a4350; outline: none; } -.onlay, .inlay .selected, .inlay > li:hover, .inlay > li.active { +.onlay, .inlay .selected, .inlay:not(.reversed) > li:hover, .inlay > li.active, .inlay.reversed > li { border-radius:3px; border: 1px solid #252930; box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2); @@ -368,6 +450,12 @@ body > .spinner, .mask{ rgb(73,83,98) 100% ); } +.onlay:active, .inlay.reversed > li:active { + color: #fff; + border: 1px solid transparent; + background-color: #282d34; + box-shadow: inset 0 1px 8px rgba(0,0,0,0.25), 0 1px 0px rgba(255,255,255,0.25); +} .question { display: block; @@ -405,7 +493,7 @@ body > .spinner, .mask{ .question .answer:hover { background: #f1f1f1; } - + .question .answer.delete { background-color: #a82f12; } @@ -413,3 +501,93 @@ body > .spinner, .mask{ margin-top: 20px; background-color: #4c5766; } + + .more_menu { + display: inline-block; + vertical-align: middle; + } + + .more_menu > a { + display: block; + background: url('../images/sprite.png') no-repeat center -137px; + height: 25px; + width: 25px; + border: 1px solid rgba(0,0,0,0.3); + transition: all 0.3s ease-in-out; + } + .more_menu.show > a:not(:active), .more_menu > a:hover:not(:active) { + background-color: #406db8; + } + + .more_menu .wrapper { + display: none; + border: 1px solid #333; + background: rgba(255,255,255,0.98); + border-radius: 3px; + padding: 4px !important; + position: absolute; + z-index: 9; + margin: 32px 0 0 -145px; + width: 185px; + box-shadow: 0 10px 10px -5px rgba(0,0,0,0.4); + text-align: center; + color: #000; + text-shadow: none; + background-image: -webkit-gradient( + linear, + left bottom, + right top, + color-stop(0, rgb(200,200,200)), + color-stop(1, rgb(255,255,255)) + ); + background-image: -moz-linear-gradient( + left bottom, + rgb(200,200,200) 0%, + rgb(255,255,255) 100% + ); + } + + .more_menu .wrapper:before { + content: ' '; + height: 0; + position: relative; + width: 0; + border: 6px solid transparent; + border-bottom-color: #fff; + display: block; + top: -16px; + left: 146px; + } + .more_menu.show .wrapper { + display: block; + } + + .more_menu ul { + padding: 0; + margin: -12px 0 0 0; + list-style: none; + } + + .more_menu .wrapper li { + width: 100%; + height: auto; + } + + .more_menu .wrapper li a { + display: block; + border-bottom: 1px solid rgba(255,255,255,0.2); + box-shadow: none; + font-weight: normal; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 1px; + padding: 3px 0; + color: #000; + } + + .more_menu .wrapper li:last-child a { + border: none; + } + .more_menu .wrapper li a:hover { + background: rgba(0,0,0,0.05); + } \ No newline at end of file diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css index 1821c412..464be778 100644 --- a/couchpotato/static/style/page/settings.css +++ b/couchpotato/static/style/page/settings.css @@ -1,9 +1,3 @@ -/* @override - http://localhost:5000/static/style/page/settings.css - http://192.168.1.20:5000/static/style/page/settings.css - http://127.0.0.1:5000/static/style/page/settings.css -*/ - .page.settings:after { content: "."; display: block; @@ -16,22 +10,22 @@ .page.settings .tabs { float: left; width: 20%; - font-size: 25px; + font-size: 20px; text-align: right; list-style: none; padding: 40px 0; margin: 0; min-height: 470px; - + background-image: -webkit-gradient( linear, right top, 40% 4%, color-stop(0, rgba(0,0,0, 0.3)), - color-stop(0.9, rgba(0,0,0, 0)) + color-stop(1, rgba(0,0,0, 0)) ); background-image: -moz-linear-gradient( - 30% 0% 16deg, + 10% 0% 16deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.3) 100% ); @@ -39,26 +33,56 @@ .page.settings .tabs a { display: block; padding: 11px 15px; - color: #fff; - } - .page.settings .tabs .active a { - background: #4e5969; + font-weight: normal; + transition: all 0.1s ease-in-out; + color: rgba(255, 255, 255, 0.8); } + .page.settings .tabs a:hover, .page.settings .tabs .active a { + background: rgb(78, 89, 105); + font-weight: bold; + font-size: 25px; + color: #fff; + } + + .page.settings .tabs .subtabs { + list-style: none; + padding: 0; + overflow: hidden; + transition: all 1s ease-in-out; + max-height: 0; + } + .page.settings .tabs > .active .subtabs { + max-height: 300px; + } + + .page.settings .tabs .subtabs a { + font-size: 15px; + padding: 1px 15px; + font-weight: normal; + color: rgba(255, 255, 255, 0.8); + background: rgba(78, 89, 105, 0.4); + } + + .page.settings .tabs .subtabs .active a { + font-weight: bold; + color: #fff; + background: rgb(78, 89, 105); + } + - .page.settings .containers { - width: 75.8%; + width: 80%; float: left; padding: 20px 2%; min-height: 300px; - } + } - .page .advanced { + .page .advanced { display: none; color: #edc07f; } .page.show_advanced .advanced { display: block; } - + .page.settings .tab_content { display: none; } @@ -79,7 +103,7 @@ font-size: 12px; margin-left: 10px; } - + .page fieldset.disabled .ctrlHolder { display: none; } @@ -117,22 +141,6 @@ vertical-align: middle; padding-left: 2%; } - - .check { - display: inline-block; - vertical-align: middle; - height: 16px; - width: 16px; - cursor: pointer; - } - .check.highlighted { background-color: #424c59; } - .check.checked { - background-image: url('../../images/checks.png'); - background-position: -2px 0; -} - .check input { - display: none; - } .page .check + .formHint { float: none; @@ -142,14 +150,14 @@ height: 24px; vertical-align: middle; } - + .page .ctrlHolder label { font-weight: bold; width: 20%; margin: 0; padding: 6px 0 0; } - + .page .xsmall { width: 20px !important; text-align: center; } .page input[type=text], .page input[type=password] { @@ -163,7 +171,7 @@ .page .input.medium { width: 15% } .page .input.large { width: 25% } .page .input.xlarge { width: 30% } - + .page .advanced_toggle { clear: both; display: block; @@ -172,15 +180,15 @@ margin: 0; } .page .advanced_toggle span { padding: 0 5px; } - .page.show_advanced .advanced_toggle { + .page.show_advanced .advanced_toggle { color: #edc07f; } - + .page .directory { display: inline-block; padding: 0 4% 0 4px; font-size: 13px; - width: 26.3%; + width: 30%; background-image: url('../../images/icon.folder.gif'); background-repeat: no-repeat; background-position: 97% center; @@ -195,7 +203,7 @@ white-space: nowrap; cursor: pointer; } - + .page .directory_list { z-index: 2; position: absolute; @@ -205,7 +213,7 @@ border-radius: 3px; box-shadow: 0 0 50px rgba(0,0,0,0.55); } - + .page .directory_list .pointer { border-right: 6px solid transparent; border-left: 6px solid transparent; @@ -215,7 +223,7 @@ width: 0px; margin: -6px 0 0 38%; } - + .page .directory_list ul { width: 92%; height: 300px; @@ -223,7 +231,7 @@ margin: 0 4%; font-size: 16px; } - + .page .directory_list li { padding: 4px 10px; cursor: pointer; @@ -234,17 +242,17 @@ .page .directory_list li:last-child { border-bottom: 1px solid rgba(255,255,255,0.1); } - + .page .directory_list li:hover { background-color: #515c68; } - + .page .directory_list .actions { clear: both; padding: 4% 4% 2%; min-height: 25px; } - + .page .directory_list .actions label { float: right; width: auto; @@ -253,7 +261,7 @@ .page .directory_list .actions .inlay { margin: -2px 0 0 7px; } - + .page .directory_list .actions .back { font-weight: bold; width: 160px; @@ -262,7 +270,7 @@ line-height: 120%; vertical-align: top; } - + .page .directory_list .actions:last-child { float: right; padding: 4%; @@ -272,23 +280,23 @@ padding: 0 5px; text-shadow: none; } - + .page .directory_list .actions:last-child > .clear { left: -90%; position: relative; background-color: #af3128; } - + .page .directory_list .actions:last-child > .cancel { font-weight: bold; color: #ddd; } - + .page .directory_list .actions:last-child > .save { background: #9dc156; } - - + + .page .multi_directory.is_empty .delete { visibility: hidden; } @@ -304,18 +312,18 @@ background-position: center; margin-left: 5px; } - - + + .page .tag_input select { width: 20%; display: inline-block; } - + .page .tag_input .selection { border-radius: 0 10px 10px 0; height: 26px; } - + .page .tag_input > input { display: none; } @@ -334,7 +342,7 @@ border-radius: 3px 0 0 3px; } .page .tag_input:hover .formHint { display: none; } - + .page .tag_input > ul > li { display: inline-block; min-height: 20px; @@ -354,39 +362,39 @@ border-radius: 2px; } .page .tag_input > ul:hover > li.choice { - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, rgba(255,255,255,0.1)), color-stop(1, rgba(255,255,255,0.3)) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100% ); } .page .tag_input > ul > li.choice:hover { - background: url('../images/checks.png') no-repeat 94% -53px, -webkit-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( linear, left bottom, left top, color-stop(0, #406db8), color-stop(1, #5b9bd1) ); - background: url('../images/checks.png') no-repeat 94% -53px, -moz-linear-gradient( + background: url('../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( center top, #5b9bd1 0%, #406db8 100% ); } - + .page .tag_input .select { display: none; } .page .tag_input:hover .select { display: inline-block; } - + .page .tag_input li input { background: 0; border: 0; @@ -399,13 +407,13 @@ padding-left: 2px; min-width: 0; } - + .page .tag_input li:not(.choice) span { white-space: pre; position: absolute; top: -9999px; } - + .page .tag_input .delete { display: none; height: 10px; @@ -429,11 +437,11 @@ background-size: 65%; } .page .tag_input .choice:hover .delete { display: inline-block; } - .page .tag_input .choice .delete:hover { + .page .tag_input .choice .delete:hover { height: 14px; margin-top: -13px; } - + .page .combined_table .head { margin: 0 0 0 60px; } @@ -451,17 +459,17 @@ .page .combined_table .head abbr.host { margin-right: 197px; } - + .page .combined_table .ctrlHolder { padding-top: 2px; padding-bottom: 3px; } .page .combined_table .ctrlHolder.hide { display: none; } - + .page .combined_table .ctrlHolder > * { margin: 0 10px 0 0; } - + .page .combined_table .ctrlHolder .delete { display: none; width: 22px; @@ -472,7 +480,7 @@ .page .combined_table .ctrlHolder:hover .delete { display: inline-block; } - + .page .combined_table .ctrlHolder.is_empty .delete, .page.settings .combined_table .ctrlHolder.is_empty .check { visibility: hidden; } @@ -511,7 +519,7 @@ .page .tab_about .donate form { padding: 10px 0 0; } - + .page .tab_about .info { padding: 20px 30px; margin: 0; @@ -524,7 +532,7 @@ width: 17%; font-weight: bold; } - + .page .tab_about .info dd { float: right; width: 80%; @@ -533,13 +541,50 @@ font-style: italic; } .page .tab_about .info dd.version { cursor: pointer; } - + .page .tab_about .group_actions > div { padding: 30px; text-align: center; } - + .page .tab_about .group_actions a { margin: 0 10px; font-size: 20px; + } + +.group_userscript { + background: center bottom no-repeat; + min-height: 360px; + font-size: 20px; + font-weight: normal; +} + + .group_userscript h2 .hint { + display: block; + margin: 0 !important; + } + + .group_userscript .userscript { + float: left; + margin: 14px 0 0 25px; + height: 36px; + line-height: 25px; + } + + .group_userscript .or { + float: left; + margin: 20px 10px; + } + + .group_userscript .bookmarklet { + display: block; + display: block; + float: left; + padding: 20px 15px 0 0 ; + border-radius: 5px; + } + + .group_userscript .bookmarklet span { + margin-left: 10px; + display: inline-block; } \ No newline at end of file diff --git a/couchpotato/static/style/uniform.css b/couchpotato/static/style/uniform.css index a64359cf..91bc83fc 100644 --- a/couchpotato/static/style/uniform.css +++ b/couchpotato/static/style/uniform.css @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------------ Copyright (c) 2010, Dragan Babic - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -10,10 +10,10 @@ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -28,22 +28,22 @@ /* ------------------------------------------------------------------------------ */ .uniForm{ margin: 0; padding: 0; position: relative; z-index: 1; } /* reset stuff */ - + /* Some generals and more resets */ .uniForm fieldset{ border: none; margin: 0; padding: 0; } .uniForm fieldset legend{ margin: 0; padding: 0; } - + /* This are the main units that contain form elements */ .uniForm .ctrlHolder, .uniForm .buttonHolder{ margin: 0; padding: 0; clear: both; } - - /* Clear all floats */ + + /* Clear all floats */ .uniForm:after, - .uniForm .buttonHolder:after, - .uniForm .ctrlHolder:after, + .uniForm .buttonHolder:after, + .uniForm .ctrlHolder:after, .uniForm .ctrlHolder .multiField:after, .uniForm .inlineLabel:after{ content: "."; display: block; height: 0; line-height: 0; font-size: 0; clear: both; min-height: 0; visibility: hidden; } - + .uniForm label, .uniForm button{ cursor: pointer; } @@ -55,17 +55,17 @@ .uniForm label, .uniForm .label{ display: block; float: none; margin: 0 0 .5em 0; padding: 0; line-height: 100%; width: auto; } - + /* Float the input elements */ .uniForm .textInput, .uniForm .fileUpload, .uniForm .selectInput, .uniForm select, .uniForm textarea{ float: left; width: 53%; margin: 0; } - + /* Postition the hints */ .uniForm .formHint{ float: right; width: 43%; margin: 0; clear: none; } - + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ .uniForm ul{ float: left; width: 53%; margin: 0; padding: 0; } .uniForm ul li{ margin: 0 0 .5em 0; list-style: none; } @@ -79,7 +79,7 @@ .uniForm ul.alternate .textInput, .uniForm ul.alternate .selectInput, .uniForm ul.alternate select{ width: 98%; margin-top: .5em; display: block; float: none; } - + /* Required fields asterisk styling */ .uniForm label em, .uniForm .label em{ float: left; width: 1em; margin: 0 0 0 -1em; } @@ -93,7 +93,7 @@ .uniForm .inlineLabels label, .uniForm .inlineLabels .label{ float: left; margin: .3em 2% 0 0; padding: 0; line-height: 1; position: relative; width: 32%; } - + /* Float the input elements */ .uniForm .inlineLabels .textInput, .uniForm .inlineLabels .fileUpload, @@ -103,7 +103,7 @@ /* Postition the hints */ .uniForm .inlineLabels .formHint{ clear: both; float: none; width: auto; margin-left: 34%; position: static; } - + /* Position the elements inside combo boxes (multiple inputs/selects/checkboxes/radio buttons per unit) */ .uniForm .inlineLabels ul{ float: left; width: 66%; } .uniForm .inlineLabels ul li{ margin: .5em 0; } @@ -113,7 +113,7 @@ .uniForm .inlineLabels ul li label .textInput, .uniForm .inlineLabels ul li label textarea, .uniForm .inlineLabels ul li label select{ float: none; display: block; width: 98%; } - + /* Required fields asterisk styling */ .uniForm .inlineLabels label em, .uniForm .inlineLabels .label em{ display: block; float: none; margin: 0; position: absolute; right: 0; } @@ -124,22 +124,22 @@ /* Generals */ .uniForm legend{ color: inherit; } - + .uniForm .secondaryAction{ float: left; } - + /* .inlineLabel is used for inputs within labels - checkboxes and radio buttons */ .uniForm .inlineLabel input, .uniForm .inlineLabels .inlineLabel input, .uniForm .blockLabels .inlineLabel input, /* class .inlineLabel is depreciated */ .uniForm label input{ float: none; display: inline; margin: 0; padding: 0; border: none; } - + .uniForm .buttonHolder .inlineLabel, .uniForm .buttonHolder label{ float: left; margin: .5em 0 0 0; width: auto; max-width: 60%; text-align: left; } - + /* When you don't want to use a label */ .uniForm .inlineLabels .noLabel ul{ margin-left: 34%; /* Match to width of label + gap to field */ } - + /* Classes for control of the widths of the fields */ .uniForm .small { width: 30% !important; } .uniForm .medium{ width: 45% !important; } diff --git a/couchpotato/static/style/uniform.generic.css b/couchpotato/static/style/uniform.generic.css index a532dbf1..e70a9158 100644 --- a/couchpotato/static/style/uniform.generic.css +++ b/couchpotato/static/style/uniform.generic.css @@ -1,11 +1,11 @@ /* ------------------------------------------------------------------------------ - + UNI-FORM DEFAULT by DRAGAN BABIC (v2) | Wed, 31 Mar 10 - + ------------------------------------------------------------------------------ - + Copyright (c) 2010, Dragan Babic - + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without @@ -14,10 +14,10 @@ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND @@ -26,18 +26,18 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + ------------------------------------------------------------------------------ */ .uniForm{} - + .uniForm legend{ font-weight: bold; font-size: 100%; margin: 0; padding: 1.5em 0; } - + .uniForm .ctrlHolder{ padding: 1em; border-bottom: 1px solid #efefef; } .uniForm .ctrlHolder.focused{ background: #fffcdf; } - + .uniForm .inlineLabels .noLabel{} - + .uniForm .buttonHolder{ background: #efefef; text-align: right; margin: 1.5em 0 0 0; padding: 1.5em; /* CSS3 */ border-radius: 4px; @@ -51,21 +51,21 @@ .uniForm .buttonHolder .primaryAction:active{ position: relative; top: 1px; } .uniForm .secondaryAction { text-align: left; } .uniForm button.secondaryAction { background: transparent; border: none; color: #777; margin: 1.25em 0 0 0; padding: 0; } - + .uniForm .inlineLabels label em, .uniForm .inlineLabels .label em{ font-style: normal; font-weight: bold; } .uniForm label small{ font-size: .75em; color: #777; } - + .uniForm .textInput, .uniForm textarea { padding: 4px 2px; border: 1px solid #aaa; background: #fff; } .uniForm textarea { height: 12em; } .uniForm select {} .uniForm .fileUpload {} - + .uniForm ul{} .uniForm li{} .uniForm ul li label{ font-size: .85em; } - + .uniForm .small {} .uniForm .medium{} .uniForm .large {} /* Large is default and should match the value you set for .textInput, textarea or select */ @@ -73,15 +73,15 @@ .uniForm .small, .uniForm .medium, .uniForm .auto{} - + /* Get rid of the 'glow' effect in WebKit, optional */ .uniForm .ctrlHolder .textInput:focus, .uniForm .ctrlHolder textarea:focus{ outline: none; } - + .uniForm .formHint { font-size: .85em; color: #777; } .uniForm .inlineLabels .formHint { padding-top: .5em; } .uniForm .ctrlHolder.focused .formHint{ color: #333; } - + /* ----------------------------------------------------------------------------- */ /* ############################### Messages #################################### */ /* ----------------------------------------------------------------------------- */ @@ -105,7 +105,7 @@ -o-border-radius: 4px; -khtml-border-radius: 4px; } - + .uniForm .ctrlHolder.error, .uniForm .ctrlHolder.focused.error{ background: #ffdfdf; border: 1px solid #f3afb5; /* CSS3 */ @@ -118,7 +118,7 @@ .uniForm .ctrlHolder.error input.error, .uniForm .ctrlHolder.error select.error, .uniForm .ctrlHolder.error textarea.error{ color: #af4c4c; margin: 0 0 6px 0; padding: 4px; } - + /* Success messages at the top of the form */ .uniForm #okMsg{ background: #c8ffbf; border: 1px solid #a2ef95; margin: 0 0 1.5em 0; padding: 0 1.5em; text-align: center; /* CSS3 */ diff --git a/couchpotato/templates/_desktop.html b/couchpotato/templates/_desktop.html index 28beaa10..63aa4dd8 100644 --- a/couchpotato/templates/_desktop.html +++ b/couchpotato/templates/_desktop.html @@ -10,9 +10,9 @@ + {% if not env.get('dev') %} - - + {% endif %} @@ -20,6 +20,7 @@ + @@ -29,6 +30,7 @@ + @@ -49,6 +51,7 @@ new Uniform(); Api.setup({ + 'host': {{ fireEvent('app.api_url', single = True)|tojson|safe }}, 'url': {{ url_for('api.index')|tojson|safe }}, 'path_sep': {{ sep|tojson|safe }}, 'is_remote': false @@ -87,6 +90,7 @@ 'options': "{{ env.get('options')|safe }}", 'app_dir': {{ env.get('app_dir')|tojson|safe }}, 'data_dir': {{ env.get('data_dir')|tojson|safe }}, + 'pid': {{ env.getPid()|tojson|safe }}, 'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }} }); }) diff --git a/couchpotato/templates/api.html b/couchpotato/templates/api.html new file mode 100644 index 00000000..ec067f95 --- /dev/null +++ b/couchpotato/templates/api.html @@ -0,0 +1,64 @@ + + + + + API documentation + + + +

CouchPotato API Documentation

+
+ You can access the API via
{{ fireEvent('app.api_url', single = True)|safe }}/
+ To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome. + All the data that you see there are from the API. +
+
+ A normal API call: +
{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/
+
+ You can also use the API over another domain using JSONP, the callback function should be in 'callback_func' +
{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction
+
+ + {% for route in routes %} + {% if api_docs.get(route) %} +
+

{{route}}

+
{{api_docs[route].get('desc', '')}}
+ + {% if api_docs[route].get('params') %} +

Params

+ + {% for param in api_docs[route]['params'] %} + + + + + + {% endfor %} +
{{param}}{{ api_docs[route]['params'][param].get('type', 'string') }}{{ api_docs[route]['params'][param]['desc'] }}
+ {% endif %} + + {% if api_docs[route].get('return') %} +

Return

+
+
{{ api_docs[route]['return'].get('type', '{"success": True}') }}
+ {% if api_docs[route]['return'].get('example') %} +
+

Example

+
{{ api_docs[route]['return'].get('example', '')|safe }}
+
+ {% endif %} +
+ {% endif %} +
+ {% endif %} + {% endfor %} + +
+

Missing documentation

+ {{', '.join(api_docs_missing)}} +
+ + + \ No newline at end of file diff --git a/libs/axl/axel.py b/libs/axl/axel.py index 59ffa1be..5607450d 100644 --- a/libs/axl/axel.py +++ b/libs/axl/axel.py @@ -141,7 +141,7 @@ class Event(object): def fire(self, *args, **kwargs): """ Stores all registered handlers in a queue for processing """ self.queue = Queue.Queue() - self.result = [] + self.result = {} if self.handlers: @@ -158,12 +158,12 @@ class Event(object): if self.asynchronous: handler_, memoize, timeout = self.handlers[handler] - self.result.append((None, None, handler_)) + self.result[handler] = (None, None, handler_) if not self.asynchronous: self.queue.join() - return tuple(self.result) or None + return self.result or None def count(self): """ Returns the count of registered handlers """ @@ -187,12 +187,12 @@ class Event(object): try: r = self._memoize(memoize, timeout, handler, *args, **kwargs) if not self.asynchronous: - self.result.append(tuple(r)) + self.result[h_] = tuple(r) except Exception: if not self.asynchronous: - self.result.append((False, self._error(sys.exc_info()), - handler)) + self.result[h_] = (False, self._error(sys.exc_info()), + handler) else: self.error_handler(sys.exc_info()) finally: diff --git a/libs/git/repository.py b/libs/git/repository.py index 669e8a8b..b6609e3f 100755 --- a/libs/git/repository.py +++ b/libs/git/repository.py @@ -144,7 +144,7 @@ class LocalRepository(Repository): def getGitVersion(self): if self._version is None: version_output = self._getOutputAssertSuccess("version") - version_match = re.match(r"git\s+version\s+(\S+)$", version_output, re.I) + version_match = re.match(r"git\s+version\s+(\S+)[\s\(]?", version_output, re.I) if version_match is None: raise GitException("Cannot extract git version (unfamiliar output format %r?)" % version_output) self._version = version_match.group(1) diff --git a/libs/pynmwp/__init__.py b/libs/pynmwp/__init__.py new file mode 100644 index 00000000..de724b9d --- /dev/null +++ b/libs/pynmwp/__init__.py @@ -0,0 +1,134 @@ +from xml.dom.minidom import parseString +from httplib import HTTPSConnection +from urllib import urlencode + +__version__ = "0.1" + +API_SERVER = 'notifymywindowsphone.com' +ADD_PATH = '/publicapi/notify' + +USER_AGENT = "PyNMWP/v%s" % __version__ + +def uniq_preserve(seq): # Dave Kirby + # Order preserving + seen = set() + return [x for x in seq if x not in seen and not seen.add(x)] + +def uniq(seq): + # Not order preserving + return {}.fromkeys(seq).keys() + +class PyNMWP(object): + """PyNMWP(apikey=[], developerkey=None) +takes 2 optional arguments: + - (opt) apykey: might me a string containing 1 key or an array of keys + - (opt) developerkey: where you can store your developer key +""" + + def __init__(self, apikey = [], developerkey = None): + self._developerkey = None + self.developerkey(developerkey) + if apikey: + if type(apikey) == str: + apikey = [apikey] + self._apikey = uniq(apikey) + + def addkey(self, key): + "Add a key (register ?)" + if type(key) == str: + if not key in self._apikey: + self._apikey.append(key) + elif type(key) == list: + for k in key: + if not k in self._apikey: + self._apikey.append(k) + + def delkey(self, key): + "Removes a key (unregister ?)" + if type(key) == str: + if key in self._apikey: + self._apikey.remove(key) + elif type(key) == list: + for k in key: + if key in self._apikey: + self._apikey.remove(k) + + def developerkey(self, developerkey): + "Sets the developer key (and check it has the good length)" + if type(developerkey) == str and len(developerkey) == 48: + self._developerkey = developerkey + + def push(self, application = "", event = "", description = "", url = "", priority = 0, batch_mode = False): + """Pushes a message on the registered API keys. +takes 5 arguments: + - (req) application: application name [256] + - (req) event: event name [1000] + - (req) description: description [10000] + - (opt) url: url [512] + - (opt) priority: from -2 (lowest) to 2 (highest) (def:0) + - (opt) batch_mode: call API 5 by 5 (def:False) + +Warning: using batch_mode will return error only if all API keys are bad + cf: http://nma.usk.bz/api.php +""" + datas = { + 'application': application[:256].encode('utf8'), + 'event': event[:1024].encode('utf8'), + 'description': description[:10000].encode('utf8'), + 'priority': priority + } + + if url: + datas['url'] = url[:512] + + if self._developerkey: + datas['developerkey'] = self._developerkey + + results = {} + + if not batch_mode: + for key in self._apikey: + datas['apikey'] = key + res = self.callapi('POST', ADD_PATH, datas) + results[key] = res + else: + for i in range(0, len(self._apikey), 5): + datas['apikey'] = ",".join(self._apikey[i:i + 5]) + res = self.callapi('POST', ADD_PATH, datas) + results[datas['apikey']] = res + return results + + def callapi(self, method, path, args): + headers = { 'User-Agent': USER_AGENT } + if method == "POST": + headers['Content-type'] = "application/x-www-form-urlencoded" + http_handler = HTTPSConnection(API_SERVER) + http_handler.request(method, path, urlencode(args), headers) + resp = http_handler.getresponse() + + try: + res = self._parse_reponse(resp.read()) + except Exception, e: + res = {'type': "pynmwperror", + 'code': 600, + 'message': str(e) + } + pass + + return res + + def _parse_reponse(self, response): + root = parseString(response).firstChild + for elem in root.childNodes: + if elem.nodeType == elem.TEXT_NODE: continue + if elem.tagName == 'success': + res = dict(elem.attributes.items()) + res['message'] = "" + res['type'] = elem.tagName + return res + if elem.tagName == 'error': + res = dict(elem.attributes.items()) + res['message'] = elem.firstChild.nodeValue + res['type'] = elem.tagName + return res +