Merge branch 'refs/heads/develop' into desktop

Conflicts:
	CouchPotato.py
	couchpotato/core/plugins/renamer/main.py
	couchpotato/core/plugins/trailer/__init__.py
This commit is contained in:
Ruud
2012-04-07 21:35:36 +02:00
129 changed files with 5318 additions and 2598 deletions
Regular → Executable
+124 -110
View File
@@ -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
+16
View File
@@ -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')
+13 -12
View File
@@ -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)
+5 -4
View File
@@ -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. <a target="_self" href="/docs/">Docs</a>',
},
{
'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',
},
-45
View File
@@ -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
+19 -7
View File
@@ -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')
+1 -1
View File
@@ -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)
+30 -8
View File
@@ -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())
@@ -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);
}
}
});
}
+9 -6
View File
@@ -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
@@ -5,6 +5,7 @@ def start():
config = [{
'name': 'blackhole',
'order': 30,
'groups': [
{
'tab': 'downloaders',
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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')):
+6 -5
View File
@@ -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]:
+1 -1
View File
@@ -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:
+7 -2
View File
@@ -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()))
+7 -6
View File
@@ -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
+100 -7
View File
@@ -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,
@@ -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)
})
},
+1 -1
View File
@@ -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()
@@ -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.',
},
],
}
],
}]
@@ -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
+1 -1
View File
@@ -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
@@ -5,6 +5,7 @@ def start():
config = [{
'name': 'automation',
'order': 30,
'groups': [
{
'tab': 'automation',
+1 -1
View File
@@ -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)
+22 -3
View File
@@ -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
+7 -1
View File
@@ -19,7 +19,13 @@ class FileManager(Plugin):
addEvent('file.download', self.download)
addEvent('file.types', self.getTypes)
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True)
addApiView('file.cache/<path:filename>', 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 = ''):
+2 -2
View File
@@ -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', {
+3 -2
View File
@@ -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),
+21 -3
View File
@@ -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):
+3 -3
View File
@@ -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; }
@@ -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;
+1 -2
View File
@@ -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',
+6 -1
View File
@@ -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)
+108 -33
View File
@@ -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,
+215 -21
View File
@@ -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){
+287 -70
View File
@@ -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;
}
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;
}
+75 -19
View File
@@ -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
}
@@ -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;
}
+76 -27
View File
@@ -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
}
@@ -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;
@@ -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
+5 -1
View File
@@ -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)
@@ -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;
@@ -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;
},
+34 -2
View File
@@ -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()
@@ -24,6 +24,7 @@ rename_options = {
config = [{
'name': 'renamer',
'order': 40,
'description': 'Move and rename your downloaded movies to your movie directory.',
'groups': [
{
+41 -6
View File
@@ -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()))
+3 -2
View File
@@ -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 {}
+7 -1
View File
@@ -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
+23 -2
View File
@@ -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
@@ -6,6 +6,7 @@ def start():
config = [{
'name': 'searcher',
'order': 20,
'groups': [
{
'tab': 'searcher',
+9 -3
View File
@@ -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():
+8 -1
View File
@@ -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):
@@ -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();
@@ -8,7 +8,9 @@ config = [{
'groups': [
{
'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle',
'label': 'Download subtitles after rename',
'options': [
{
'name': 'enabled',
+3 -1
View File
@@ -7,8 +7,10 @@ config = [{
'name': 'trailer',
'groups': [
{
'tab': 'metadata',
'tab': 'renamer',
'subtab': 'trailer',
'name': 'trailer',
'label': 'Download trailer after rename',
'options': [
{
'name': 'enabled',
@@ -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..')
+18 -5
View File
@@ -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/<path:filename>', self.getUserScript, static = True)
addApiView('userscript.get/<random>/<path:filename>', 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):
@@ -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});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+39 -18
View File
@@ -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': '<img src="' + close_img + '" />',
'id': 'close_button',
'onclick': function(){
popup.innerHTML = '';
popup.appendChild(add_button);
}
}));
popup.appendChild(iframe)
}
var add_button = create('a', {
'innerHTML': '<img src="' + cp_icon + '" />',
'id': 'add_to',
'onclick': function(){
popup.innerHTML = '';
popup.appendChild(create('a', {
'innerHTML': '<img src="' + close_img + '" />',
'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();
setVersion();
@@ -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;
}
@@ -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();
@@ -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 = []
+1 -1
View File
@@ -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)
+1 -6
View File
@@ -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:
@@ -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',
@@ -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',
@@ -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',
@@ -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',
@@ -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', []):
@@ -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', ''))),
@@ -10,7 +10,7 @@ config = [{
'tab': 'providers',
'name': 'tmdb',
'label': 'TheMovieDB',
'advanced': True,
'hidden': True,
'description': 'Used for all calls to TheMovieDB.',
'options': [
{
@@ -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 [],
},
@@ -7,7 +7,8 @@ config = [{
'name': 'moovee',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.moovee',
'description': 'SD movies only',
'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'mysterbin',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'Mysterbin',
'description': '',
'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'newzbin',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'newzbin',
'wizard': True,
'options': [
@@ -26,6 +26,7 @@ class Newzbin(NZBProvider, RSS):
1024: ['r5'],
}
cat_ids = [
([262144], ['bd50']),
([2097152], ['1080p']),
([524288], ['720p']),
([262144], ['brrip']),
@@ -7,7 +7,8 @@ config = [{
'name': 'newznab',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'newznab',
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>',
'wizard': True,
@@ -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
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbclub',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'NZBClub',
'description': '',
'options': [
@@ -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)
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbindex',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbindex',
'description': 'Free provider, but less accurate.',
'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbmatrix',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbmatrix',
'label': 'NZBMatrix',
'wizard': True,
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbs',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbs',
'description': 'Id and Key can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">on your nzbs.org RSS page</a>.',
'wizard': True,
@@ -7,7 +7,8 @@ config = [{
'name': 'x264',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.hdtv.x264',
'description': 'HD movies only',
'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'kickasstorrents',
'groups': [
{
'tab': 'providers',
'tab': 'searcher',
'subtab': 'providers',
'name': 'KickAssTorrents',
'options': [
{
@@ -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
@@ -9,7 +9,7 @@ class AppleTrailers(UserscriptBase):
def getMovie(self, url):
try:
data = self.urlopen(url)
data = self.getUrl(url)
except:
return
@@ -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))
@@ -0,0 +1,6 @@
from .main import Letterboxd
def start():
return Letterboxd()
config = []
@@ -0,0 +1,6 @@
from couchpotato.core.providers.userscript.base import UserscriptBase
class Letterboxd(UserscriptBase):
includes = ['*://letterboxd.com/film/*']
@@ -0,0 +1,6 @@
from .main import RottenTomatoes
def start():
return RottenTomatoes()
config = []
@@ -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'])
@@ -10,4 +10,7 @@ class TMDB(UserscriptBase):
def getMovie(self, url):
match = re.search('(?P<id>\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'])
+35 -4
View File
@@ -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()
+43 -17
View File
@@ -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."""
+18 -2
View File
@@ -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
+39 -16
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More