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 committed 2012-04-07 21:35:36 +02:00
commit 5b6792dc20
129 files changed
+5318 -2598

No files matched your search

Regular → Executable
+124 -110
View File
@@ -4,124 +4,138 @@ from os.path import dirname
import logging import logging
import os import os
import signal import signal
import socket
import subprocess import subprocess
import sys import sys
import traceback import traceback
 
 
# Root path # Root path
base_path = dirname(os.path.abspath(__file__)) base_path = dirname(os.path.abspath(__file__))
 
# Insert local directories into path # Insert local directories into path
sys.path.insert(0, os.path.join(base_path, 'libs')) sys.path.insert(0, os.path.join(base_path, 'libs'))
 
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.variable import getDataDir from couchpotato.core.helpers.variable import getDataDir
 
class Loader(object): class Loader(object):
 
    do_restart = False do_restart = False
 
    def __init__(self): def __init__(self):
 
        # Get options via arg # Get options via arg
        from couchpotato.runner import getOptions from couchpotato.runner import getOptions
        self.options = getOptions(base_path, sys.argv[1:]) self.options = getOptions(base_path, sys.argv[1:])
 
        # Load settings # Load settings
        settings = Env.get('settings') settings = Env.get('settings')
        settings.setFile(self.options.config_file) settings.setFile(self.options.config_file)
 
        # Create data dir if needed # Create data dir if needed
        self.data_dir = os.path.expanduser(Env.setting('data_dir')) self.data_dir = os.path.expanduser(Env.setting('data_dir'))
        if self.data_dir == '': if self.data_dir == '':
            self.data_dir = getDataDir() self.data_dir = getDataDir()
 
        if not os.path.isdir(self.data_dir): if not os.path.isdir(self.data_dir):
            os.makedirs(self.data_dir) os.makedirs(self.data_dir)
 
        # Create logging dir # Create logging dir
        self.log_dir = os.path.join(self.data_dir, 'logs'); self.log_dir = os.path.join(self.data_dir, 'logs');
        if not os.path.isdir(self.log_dir): if not os.path.isdir(self.log_dir):
            os.mkdir(self.log_dir) os.mkdir(self.log_dir)
 
        # Logging # Logging
        from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
        self.log = CPLog(__name__) self.log = CPLog(__name__)
 
        formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S') 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 = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10)
        hdlr.setLevel(logging.CRITICAL) hdlr.setLevel(logging.CRITICAL)
        hdlr.setFormatter(formatter) hdlr.setFormatter(formatter)
        self.log.logger.addHandler(hdlr) self.log.logger.addHandler(hdlr)
 
    def addSignals(self): def addSignals(self):
 
        signal.signal(signal.SIGINT, self.onExit) signal.signal(signal.SIGINT, self.onExit)
        signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1)) signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
 
        from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
        addEvent('app.after_shutdown', self.afterShutdown) addEvent('app.after_shutdown', self.afterShutdown)
 
    def afterShutdown(self, restart): def afterShutdown(self, restart):
        self.do_restart = restart self.do_restart = restart
 
    def onExit(self, signal, frame): def onExit(self, signal, frame):
        from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
        fireEvent('app.crappy_shutdown', single = True) fireEvent('app.crappy_shutdown', single = True)
 
    def run(self): def run(self):
 
        self.addSignals() self.addSignals()
 
        from couchpotato.runner import runCouchPotato 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) runCouchPotato(self.options, base_path, sys.argv[1:], data_dir = self.data_dir, log_dir = self.log_dir, Env = Env)
 
        if self.do_restart: if self.do_restart:
            self.restart() self.restart()
 
    def restart(self): def restart(self):
        try: try:
            # remove old pidfile first # remove old pidfile first
            try: try:
                if self.runAsDaemon(): if self.runAsDaemon():
                    self.daemon.delpid() try: self.daemon.stop()
            except: except: pass
                self.log.critical(traceback.format_exc()) self.daemon.delpid()
  except:
            args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:] self.log.critical(traceback.format_exc())
            subprocess.Popen(args)
        except: args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
            self.log.critical(traceback.format_exc()) subprocess.Popen(args)
  except:
    def daemonize(self): self.log.critical(traceback.format_exc())
 
        if self.runAsDaemon(): def daemonize(self):
            try:
                from daemon import Daemon if self.runAsDaemon():
                self.daemon = Daemon(self.options.pid_file) try:
                self.daemon.daemonize() from daemon import Daemon
            except SystemExit: self.daemon = Daemon(self.options.pid_file)
                raise self.daemon.daemonize()
            except: except SystemExit:
                self.log.critical(traceback.format_exc()) raise
  except:
    def runAsDaemon(self): self.log.critical(traceback.format_exc())
        return self.options.daemon and  self.options.pid_file
  def runAsDaemon(self):
  return self.options.daemon and self.options.pid_file
if __name__ == '__main__': if __name__ == '__main__':
    try: try:
        l = Loader() l = Loader()
        l.daemonize() l.daemonize()
        l.run() l.run()
    except KeyboardInterrupt: except KeyboardInterrupt:
        pass pass
    except SystemExit: except SystemExit:
        raise raise
    except Exception as (nr, msg): except socket.error as (nr, msg):
        if nr != 4: # log when socket receives SIGINT, but continue.
            try: # previous code would have skipped over other types of IO errors too.
                l.log.critical(traceback.format_exc()) if nr != 4:
            except: try:
                print traceback.format_exc() 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.auth import requires_auth
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -35,6 +36,21 @@ def addView(route, func, static = False):
def index(): def index():
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) 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) @app.errorhandler(404)
def page_not_found(error): def page_not_found(error):
index_url = url_for('web.index') 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.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 = Blueprint('api', __name__)
api_docs = {}
api_docs_missing = []
def addApiView(route, func, static = False): 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) 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 """ """ Api view """
def index(): def index():
from couchpotato import app index_url = url_for('web.index')
return redirect(index_url + 'docs/')
routes = []
for route, x in sorted(app.view_functions.iteritems()):
if route[0:4] == 'api.':
routes += [route[4:]]
return jsonified({'routes': routes})
addApiView('', index) addApiView('', index)
addApiView('default', index)
+5 -4
View File
@@ -6,6 +6,7 @@ def start():
config = [{ config = [{
'name': 'core', 'name': 'core',
'order': 1,
'groups': [ 'groups': [
{ {
'tab': 'general', 'tab': 'general',
@@ -54,7 +55,7 @@ config = [{
'name': 'api_key', 'name': 'api_key',
'default': uuid4().hex, 'default': uuid4().hex,
'readonly': 1, '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', 'name': 'debug',
@@ -80,13 +81,13 @@ config = [{
}, },
{ {
'name': 'permission_folder', 'name': 'permission_folder',
'default': 0755, 'default': '0755',
'label': 'Folder CHMOD', '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', 'name': 'permission_file',
'default': 0755, 'default': '0755',
'label': 'File CHMOD', 'label': 'File CHMOD',
'description': 'Same as Folder CHMOD but for files', '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 traceback
import webbrowser import webbrowser
if os.name == 'nt':
import getppid
log = CPLog(__name__) log = CPLog(__name__)
class Core(Plugin): class Core(Plugin):
ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown'] ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
shutdown_started = False shutdown_started = False
def __init__(self): def __init__(self):
addApiView('app.shutdown', self.shutdown) addApiView('app.shutdown', self.shutdown, docs = {
addApiView('app.restart', self.restart) 'desc': 'Shutdown the app.',
addApiView('app.available', self.available) '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_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart) addEvent('app.crappy_restart', self.crappyRestart)
@@ -51,6 +56,9 @@ class Core(Plugin):
}) })
def crappyShutdown(self): def crappyShutdown(self):
if self.shutdown_started:
return
try: try:
self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False) self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
return True return True
@@ -59,6 +67,9 @@ class Core(Plugin):
return False return False
def crappyRestart(self): def crappyRestart(self):
if self.shutdown_started:
return
try: try:
self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False) self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False)
return True return True
@@ -77,6 +88,7 @@ class Core(Plugin):
def initShutdown(self, restart = False): def initShutdown(self, restart = False):
if self.shutdown_started: if self.shutdown_started:
log.info('Already shutting down') log.info('Already shutting down')
return
log.info('Shutting down' if not restart else 'Restarting') log.info('Shutting down' if not restart else 'Restarting')
+1 -1
View File
@@ -15,7 +15,7 @@ class Scheduler(Plugin):
def __init__(self): def __init__(self):
logging.getLogger('apscheduler').setLevel(logging.WARNING) logging.getLogger('apscheduler').setLevel(logging.ERROR)
addEvent('schedule.cron', self.cron) addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval) 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.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from git.repository import LocalRepository from git.repository import LocalRepository
from datetime import datetime
import os import os
import time import time
import traceback import traceback
@@ -29,18 +30,34 @@ class Updater(Plugin):
addEvent('app.load', self.check) 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.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): def getInfo(self):
return jsonified({ return jsonified(self.info())
def info(self):
return {
'repo_name': self.repo_name, 'repo_name': self.repo_name,
'last_check': self.last_check, 'last_check': self.last_check,
'update_version': self.update_version, 'update_version': self.update_version,
'version': self.getVersion() 'version': self.getVersion()
}) }
def getVersion(self): def getVersion(self):
@@ -78,14 +95,14 @@ class Updater(Plugin):
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8])) log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate(): 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.conf('automatic') and not self.update_failed:
if self.doUpdate(): if self.doUpdate():
fireEventAsync('app.crappy_restart') fireEventAsync('app.crappy_restart')
else: else:
self.update_version = {
'hash': remote.hash[:8],
'date': remote.getDate(),
}
if self.conf('notification'): if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.getVersion()) fireEvent('updater.available', message = 'A new update is available', data = self.getVersion())
@@ -106,11 +123,16 @@ class Updater(Plugin):
self.repo.saveStash() self.repo.saveStash()
log.info('Updating to latest version') log.info('Updating to latest version')
info = self.info()
self.repo.pull() self.repo.pull()
# Delete leftover .pyc files # Delete leftover .pyc files
self.deletePyc() 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 return True
except: except:
log.error('Failed updating via GIT: %s' % traceback.format_exc()) log.error('Failed updating via GIT: %s' % traceback.format_exc())
@@ -76,32 +76,10 @@ var UpdaterBase = new Class({
Api.request('updater.update', { Api.request('updater.update', {
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success){
App.restart(); App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App);
$(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)
} }
} }
}); });
} }
+9 -6
View File
@@ -18,9 +18,12 @@ class Downloader(Plugin):
def download(self, data = {}): def download(self, data = {}):
pass pass
def createFileName(self, data, filename, movie): def createNzbName(self, data, movie):
name = os.path.join('%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))) return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))
if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filename:
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, 'rar')
return '%s.%s' % (name, data.get('type')) return '%s.%s' % (name, data.get('type'))
@@ -30,10 +33,10 @@ class Downloader(Plugin):
return '' return ''
def isCorrectType(self, type): def isCorrectType(self, item_type):
is_correct = type in self.type is_correct = item_type in self.type
if not is_correct: if not is_correct:
log.debug("Downloader doesn't support this type") log.debug("Downloader doesn't support this type")
return bool return is_correct
@@ -5,6 +5,7 @@ def start():
config = [{ config = [{
'name': 'blackhole', 'name': 'blackhole',
'order': 30,
'groups': [ 'groups': [
{ {
'tab': 'downloaders', 'tab': 'downloaders',
+1 -1
View File
@@ -22,7 +22,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.' % data.get('name')) log.info('Sending "%s" to NZBGet.' % data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} 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) rpc = xmlrpclib.ServerProxy(url)
try: try:
+1 -1
View File
@@ -39,7 +39,7 @@ class Sabnzbd(Downloader):
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)), 'nzbname': self.createNzbName(data, movie),
} }
if isfunction(data.get('download')): if isfunction(data.get('download')):
+6 -5
View File
@@ -1,8 +1,7 @@
from axl.axel import Event 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 from couchpotato.core.logger import CPLog
import threading import threading
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -36,7 +35,7 @@ def addEvent(name, handler, priority = 100):
return h return h
e.handle(handler, priority = priority) e.handle(createHandle, priority = priority)
def removeEvent(name, handler): def removeEvent(name, handler):
e = events[name] e = events[name]
@@ -84,7 +83,8 @@ def fireEvent(name, *args, **kwargs):
results = None results = None
# Loop over results, stop when first not None result is found. # 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: if r[0] is True and r[1] is not None:
results = r[1] results = r[1]
break break
@@ -95,7 +95,8 @@ def fireEvent(name, *args, **kwargs):
else: else:
results = [] 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]: if r[0] == True and r[1]:
results.append(r[1]) results.append(r[1])
elif r[1]: elif r[1]:
+1 -1
View File
@@ -70,7 +70,7 @@ def jsonify(mimetype, *args, **kwargs):
def jsonified(*args, **kwargs): def jsonified(*args, **kwargs):
from couchpotato.environment import Env from couchpotato.environment import Env
callback = getParam('json_callback', None) callback = getParam('callback_func', None)
if callback: if callback:
return padded_jsonify(callback, *args, **kwargs) return padded_jsonify(callback, *args, **kwargs)
else: else:
+7 -2
View File
@@ -51,8 +51,13 @@ class Loader(object):
did_save += self.loadSettings(m, module_name, save = False) did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name')) self.loadPlugins(m, plugin.get('name'))
except ImportError: except ImportError as e:
log.debug('Import error, remove the empty folder: %s' % plugin.get('module')) # 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: except:
log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc())) 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 # Attach listeners
for listener in self.listen_to: for listener in self.listen_to:
if not listener in self.dont_listen_to: if not listener in self.dont_listen_to:
addEvent(listener, self.createNotifyHandler(listener))
# Add on snatch default def createNotifyHandler(self, listener):
def notify(message, data): def notify(message, data):
if not self.conf('on_snatch', default = 1) and listener == 'movie.snatched': if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return return
return self.notify(message = message, data = data) return self.notify(message = message, data = data)
addEvent(listener, notify) return notify
def notify(self, message = '', data = {}): def notify(self, message = '', data = {}):
pass pass
+100 -7
View File
@@ -1,8 +1,13 @@
from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent 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.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -11,28 +16,104 @@ log = CPLog(__name__)
class CoreNotifier(Notification): class CoreNotifier(Notification):
messages = [] messages = []
listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
]
def __init__(self): def __init__(self):
super(CoreNotifier, self).__init__()
addEvent('notify', self.notify) addEvent('notify', self.notify)
addEvent('notify.frontend', self.frontend) 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() self.registerEvents()
def registerEvents(self): def registerEvents(self):
# Library update, frontend refresh # Library update, frontend refresh
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data)) addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
def notify(self, message = '', data = {}): def markAsRead(self):
self.add(data = { ids = getParam('ids').split(',')
'message': message,
'raw': data, 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 = {}): def frontend(self, type = 'notification', data = {}):
self.messages.append({ self.messages.append({
'time': time.time(), 'time': time.time(),
@@ -48,6 +129,18 @@ class CoreNotifier(Notification):
if message['time'] > (time.time() - 15): if message['time'] > (time.time() - 15):
messages.append(message) 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 = [] self.messages = []
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -10,19 +10,86 @@ var NotificationBase = new Class({
// Listener // Listener
App.addEvent('load', self.startInterval.bind(self)); App.addEvent('load', self.startInterval.bind(self));
App.addEvent('unload', self.stopTimer.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 // Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self)); 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(){ startInterval: function(){
var self = this; var self = this;
self.request = Api.request('core_notifier.listener', { self.request = Api.request('notification.listener', {
'initialDelay': 100, 'initialDelay': 100,
'delay': 3000, 'delay': 3000,
'data': {'init':true},
'onSuccess': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}) })
@@ -40,16 +107,12 @@ var NotificationBase = new Class({
this.request.stopTimer() this.request.stopTimer()
}, },
notify: function(data){
var self = this;
},
processData: function(json){ processData: function(json){
var self = this; var self = this;
self.request.options.data = {}
Array.each(json.result, function(result){ 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: except:
log.error('Failed register of growl: %s' % traceback.format_exc()) 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 if self.isDisabled(): return
self.register() 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: try:
self.urlopen(url, headers = headers) self.urlopen(url, headers = headers, show_error = False)
except: except:
log.error("Couldn't sent command to XBMC") log.error("Couldn't sent command to XBMC")
return False return False
@@ -5,6 +5,7 @@ def start():
config = [{ config = [{
'name': 'automation', 'name': 'automation',
'order': 30,
'groups': [ 'groups': [
{ {
'tab': 'automation', 'tab': 'automation',
+1 -1
View File
@@ -98,7 +98,7 @@ class Plugin(object):
if not headers.get('Referer'): if not headers.get('Referer'):
headers['Referer'] = urlparse(url).hostname headers['Referer'] = urlparse(url).hostname
if not headers.get('User-Agent'): 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 host = urlparse(url).hostname
self.wait(host) self.wait(host)
+22 -3
View File
@@ -6,12 +6,31 @@ import os
import string import string
if os.name == 'nt': 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): class FileBrowser(Plugin):
def __init__(self): 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): 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)): 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) dirs.append(p + os.path.sep)
return dirs return sorted(dirs)
def getFiles(self): def getFiles(self):
pass pass
+7 -1
View File
@@ -19,7 +19,13 @@ class FileManager(Plugin):
addEvent('file.download', self.download) addEvent('file.download', self.download)
addEvent('file.types', self.getTypes) 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 = ''): def showCacheFile(self, filename = ''):
+2 -2
View File
@@ -2,7 +2,7 @@ var File = new Class({
initialize: function(file){ initialize: function(file){
var self = this; var self = this;
if(!file){ if(!file){
self.el = new Element('div'); self.el = new Element('div');
return return
@@ -17,7 +17,7 @@ var File = new Class({
createImage: function(){ createImage: function(){
var self = this; var self = this;
var file_name = self.data.path.replace(/^.*[\\\/]/, ''); var file_name = self.data.path.replace(/^.*[\\\/]/, '');
self.el = new Element('div', { 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.plugins.base import Plugin
from couchpotato.core.settings.model import Library, LibraryTitle, File from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters from string import ascii_letters
import json
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -75,7 +74,7 @@ class LibraryPlugin(Plugin):
library.tagline = toUnicode(info.get('tagline', '')) library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0) library.year = info.get('year', 0)
library.status_id = done_status.get('id') library.status_id = done_status.get('id')
library.info = toUnicode(json.dumps(info)) library.info = info
db.commit() db.commit()
# Titles # Titles
@@ -85,6 +84,8 @@ class LibraryPlugin(Plugin):
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s' % titles) log.debug('Adding titles: %s' % titles)
for title in titles: for title in titles:
if not title:
continue
t = LibraryTitle( t = LibraryTitle(
title = toUnicode(title), title = toUnicode(title),
simple_title = self.simplifyTitle(title), simple_title = self.simplifyTitle(title),
+21 -3
View File
@@ -12,9 +12,27 @@ log = CPLog(__name__)
class Logging(Plugin): class Logging(Plugin):
def __init__(self): def __init__(self):
addApiView('logging.get', self.get) addApiView('logging.get', self.get, docs = {
addApiView('logging.clear', self.clear) 'desc': 'Get the full log file by number',
addApiView('logging.log', self.log) '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): def get(self):
+3 -3
View File
@@ -42,7 +42,7 @@
float: left; float: left;
width: 86%; width: 86%;
line-height: 150%; line-height: 150%;
padding: 3px 1%; padding: 3px 0;
border-top: 1px solid rgba(255, 255, 255, 0.2); border-top: 1px solid rgba(255, 255, 255, 0.2);
font-size: 11px; font-size: 11px;
font-family: Lucida Console, Monaco, Nimbus Mono L; font-family: Lucida Console, Monaco, Nimbus Mono L;
@@ -56,10 +56,10 @@
.page.log .container .time { .page.log .container .time {
clear: both; clear: both;
width: 11%; width: 14%;
color: lightgrey; color: lightgrey;
padding: 3px 0; padding: 3px 0;
} }
.page.log .container .time:last-child { display: none; } .page.log .container .time:last-child { display: none; }
@@ -4,6 +4,20 @@ Page.Log = new Class({
name: 'log', name: 'log',
title: 'Show recent logs.', 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(){ indexAction: function(){
var self = this; var self = this;
+1 -2
View File
@@ -7,10 +7,9 @@ config = [{
'name': 'manage', 'name': 'manage',
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'manage',
'label': 'movie library manager', 'label': 'movie library manager',
'description': 'Add your existing movie folders.', 'description': 'Add your existing movie folders.',
'wizard': True,
'options': [ 'options': [
{ {
'name': 'enabled', '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) fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
addEvent('manage.update', self.updateLibrary) 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'): if not Env.get('dev'):
addEvent('app.load', self.updateLibrary) addEvent('app.load', self.updateLibrary)
+108 -33
View File
@@ -26,14 +26,60 @@ class MoviePlugin(Plugin):
} }
def __init__(self): def __init__(self):
addApiView('movie.search', self.search) addApiView('movie.search', self.search, docs = {
addApiView('movie.list', self.listView) 'desc': 'Search the movie providers for a movie',
addApiView('movie.refresh', self.refresh) '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.available_chars', self.charView)
addApiView('movie.add', self.addView, docs = {
addApiView('movie.add', self.addView) 'desc': 'Add new movie to the wanted list',
addApiView('movie.edit', self.edit) 'params': {
addApiView('movie.delete', self.delete) '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.add', self.add)
addEvent('movie.get', self.get) addEvent('movie.get', self.get)
@@ -55,19 +101,11 @@ class MoviePlugin(Plugin):
if not isinstance(status, (list, tuple)): if not isinstance(status, (list, tuple)):
status = [status] status = [status]
q = db.query(Movie) \ q = db.query(Movie) \
.join(Movie.library, Library.titles) \ .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(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 = [] filter_or = []
if starts_with: if starts_with:
@@ -88,17 +126,32 @@ class MoviePlugin(Plugin):
q = q.order_by(asc(LibraryTitle.simple_title)) 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: if limit_offset:
splt = limit_offset.split(',') splt = limit_offset.split(',')
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] 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 = [] movies = []
for movie in results: 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) movies.append(temp)
return movies return movies
@@ -117,7 +170,8 @@ class MoviePlugin(Plugin):
.join(Movie.library, Library.titles) \ .join(Movie.library, Library.titles) \
.options(joinedload_all('library.titles')) \ .options(joinedload_all('library.titles')) \
.filter(LibraryTitle.default == True) \ .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() results = q.all()
@@ -230,6 +284,13 @@ class MoviePlugin(Plugin):
db.commit() 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) movie_dict = m.to_dict(self.default_dict)
if force_readd or do_search: if force_readd or do_search:
@@ -255,19 +316,31 @@ class MoviePlugin(Plugin):
params = getParams() params = getParams()
db = get_session() db = get_session()
m = db.query(Movie).filter_by(id = params.get('id')).first() available_status = fireEvent('status.get', 'available', single = True)
m.profile_id = params.get('profile_id')
# Default title ids = params.get('id').split(',')
for title in m.library.titles: for movie_id in ids:
title.default = params.get('default_title').lower() == title.title.lower()
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) # Default title
fireEventAsync('searcher.single', movie_dict) 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({ return jsonified({
'success': True, 'success': True,
@@ -280,9 +353,11 @@ class MoviePlugin(Plugin):
status = fireEvent('status.add', 'deleted', single = True) status = fireEvent('status.add', 'deleted', single = True)
movie = db.query(Movie).filter_by(id = params.get('id')).first() ids = params.get('id').split(',')
movie.status_id = status.get('id') for movie_id in ids:
db.commit() movie = db.query(Movie).filter_by(id = movie_id).first()
movie.status_id = status.get('id')
db.commit()
return jsonified({ return jsonified({
'success': True, 'success': True,
+215 -21
View File
@@ -3,8 +3,9 @@ var MovieList = new Class({
Implements: [Options], Implements: [Options],
options: { options: {
navigation: false, navigation: true,
limit: 50 limit: 50,
menu: []
}, },
movies: [], movies: [],
@@ -69,18 +70,23 @@ var MovieList = new Class({
self.scrollspy.stop(); self.scrollspy.stop();
} }
Object.each(movies, function(info){ Object.each(movies, function(movie){
// Attach proper actions // Attach proper actions
var a = self.options.actions var a = self.options.actions,
var actions = a[info.status.identifier.capitalize()] || a.Wanted || {}; status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, { var m = new Movie(self, {
'actions': actions 'actions': actions,
}, info); 'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list); $(m).inject(self.movie_list);
m.fireEvent('injected'); m.fireEvent('injected');
self.movies.include(m)
}); });
}, },
@@ -89,8 +95,12 @@ var MovieList = new Class({
var self = this; var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.current_view = self.getSavedView();
self.el.addClass(self.current_view+'_list')
self.navigation = new Element('div.alph_nav').adopt( 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': { 'events': {
'click:relay(li)': function(e, el){ 'click:relay(li)': function(e, el){
self.movie_list.empty() 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', 'placeholder': 'Search',
'events': { 'events': {
'keyup': self.search.bind(self), 'keyup': self.search.bind(self),
'change': self.search.bind(self) 'change': self.search.bind(self)
} }
})/*, }),
self.view = new Element('ul.inlay').adopt( self.navigation_menu = new Block.Menu(self),
new Element('li.list'), self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('li.thumbnails'), new Element('span.select').adopt(
new Element('li.text') 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'); ).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 // All
self.letters['all'] = new Element('li.letter_all.available.active', { self.letters['all'] = new Element('li.letter_all.available.active', {
'text': 'ALL', 'text': 'ALL',
}).inject(self.alpha); }).inject(self.navigation_alpha);
// Chars // Chars
chars.split('').each(function(c){ chars.split('').each(function(c){
@@ -124,7 +184,7 @@ var MovieList = new Class({
'text': c, 'text': c,
'class': 'letter_'+c, 'class': 'letter_'+c,
'data-letter': c 'data-letter': c
}).inject(self.alpha); }).inject(self.navigation_alpha);
}); });
// Get available chars and highlight // 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(){ reset: function(){
var self = this; var self = this;
self.navigation.getElements('.active').removeClass('active') self.movies = []
self.calculateSelected()
self.navigation_alpha.getElements('.active').removeClass('active')
self.offset = 0; self.offset = 0;
self.load_more.show(); self.load_more.show();
self.scrollspy.start(); 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(){ search: function(){
var self = this; var self = this;
if(self.search_timer) clearTimeout(self.search_timer); if(self.search_timer) clearTimeout(self.search_timer);
self.search_timer = (function(){ 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 if (search_value == self.last_search_value) return
self.reset() self.reset()
@@ -211,8 +405,8 @@ var MovieList = new Class({
loadMore: function(){ loadMore: function(){
var self = this; var self = this;
if(self.offset >= self.options.limit)
self.getMovies() self.getMovies()
}, },
store: function(movies){ 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 { .movies {
padding: 20px 0; padding: 60px 0 20px;
} }
.movies.mass_edit_list {
padding-top: 90px;
}
.movies .movie { .movies .movie {
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
margin: 10px 0; margin: 10px 0;
overflow: hidden; overflow: hidden;
width: 100%;
transition: all 0.2s linear;
} }
.movies .movie:hover { .movies .movie.list_view, .movies .movie.mass_edit_view {
border-color: #ddd #fff #fff #ddd; 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 { .movies .data {
padding: 20px; padding: 20px;
height: 140px; height: 180px;
width: 800px; width: 840px;
position: relative; position: relative;
float: right; float: right;
border-radius: 0; border-radius: 0;
-moz-border-radius: 0;
-webkit-border-radius: 0;
overflow: hidden; 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 { .movies .poster {
float: left; float: left;
width: 120px; width: 120px;
@@ -37,21 +61,37 @@
overflow: hidden; overflow: hidden;
height: 180px; height: 180px;
border-radius: 4px 0 0 4px; 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 { .movies .poster img, .options .poster img {
width: 101%; width: 101%;
height: 101%; height: 101%;
} }
.movies .info .title { .movies .info .title {
font-size: 30px; font-size: 30px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
float: left; 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 { .movies .info .year {
font-size: 30px; font-size: 30px;
margin-bottom: 10px; margin-bottom: 10px;
@@ -59,8 +99,13 @@
color: #bbb; color: #bbb;
width: 10%; width: 10%;
text-align: right; 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 { .movies .info .rating {
font-size: 30px; font-size: 30px;
margin-bottom: 10px; margin-bottom: 10px;
@@ -69,7 +114,7 @@
width: 5%; width: 5%;
padding: 0 0 0 3%; padding: 0 0 0 3%;
} }
.movies .info .description { .movies .info .description {
clear: both; clear: both;
height: 80px; height: 80px;
@@ -78,35 +123,85 @@
.movies .data:hover .description { .movies .data:hover .description {
overflow: auto; overflow: auto;
} }
.movies .list_view .info .description, .movies .mass_edit_view .info .description {
.movies .data .quality span { display: none;
padding: 0 5px; }
font-weight: bold;
.movies .data .quality {
display: block;
min-height: 20px;
vertical-align: mid;
} }
.movies .data .quality span:first-child {padding-left: 0;}
.movies .data .quality span {
.movies .data .quality .available { color: orange; } padding: 2px 3px;
.movies .data .quality .snatched { color: lightgreen; } 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 { .movies .data .actions {
line-height: 0; line-height: 0;
clear: both; clear: both;
float: right; float: right;
margin-top: -25px; margin-top: -25px;
} }
.movies .data:hover .action { opacity: 0.6; } .movies .data:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; } .movies .data:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
display: none;
}
.movies .data .action { .movies .data .action {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
display: inline-block; display: inline-block;
width: 20px; width: 26px;
height: 20px; height: 26px;
padding: 3px; padding: 3px;
opacity: 0; 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 { .movies .delete_container {
clear: both; clear: both;
text-align: center; text-align: center;
@@ -128,22 +223,23 @@
color: #fff; color: #fff;
background-color: #d32917; background-color: #d32917;
} }
.movies .options { .movies .options {
position: absolute; position: absolute;
margin-left: 120px; margin-left: 120px;
width: 840px;
} }
.movies .options .form { .movies .options .form {
margin: 70px 20px 0; margin: 70px 20px 0;
float: left; float: left;
font-size: 20px; font-size: 20px;
} }
.movies .options .form select { .movies .options .form select {
margin-right: 20px; margin-right: 20px;
} }
.movies .options .table { .movies .options .table {
height: 180px; height: 180px;
overflow: auto; overflow: auto;
@@ -151,18 +247,27 @@
.movies .options .table .item { .movies .options .table .item {
border-bottom: 1px solid rgba(255,255,255,0.1); 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: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); 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); background: rgba(255,255,255,0.03);
} }
.movies .options .table .item > * { .movies .options .table .item > * {
display: inline-block; display: inline-block;
padding: 0 5px; padding: 0 5px;
width: 50px; width: 60px;
min-height: 24px; min-height: 24px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -174,21 +279,21 @@
border: 0; border: 0;
} }
.movies .options .table .provider { .movies .options .table .provider {
width: 120px; width: 130px;
} }
.movies .options .table .name { .movies .options .table .name {
width: 360px; width: 370px;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
padding: 0 10px; padding: 0 10px;
} }
.movies .options .table.files .name { width: 598px; } .movies .options .table.files .name { width: 605px; }
.movies .options .table .type { width: 120px; } .movies .options .table .type { width: 130px; }
.movies .options .table .is_available { width: 80px; } .movies .options .table .is_available { width: 90px; }
.movies .options .table a { .movies .options .table a {
width: 16px !important; width: 30px !important;
height: 16px; height: 20px;
opacity: 0.8; opacity: 0.8;
} }
.movies .options .table a:hover { .movies .options .table a:hover {
@@ -202,7 +307,7 @@
padding-bottom: 4px; padding-bottom: 4px;
height: auto; height: auto;
} }
.movies .load_more { .movies .load_more {
display: block; display: block;
padding: 10px; padding: 10px;
@@ -214,10 +319,23 @@
} }
.movies .alph_nav { .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; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
margin: 0; margin: 0;
@@ -225,30 +343,129 @@
user-select: none; user-select: none;
} }
.movies .alph_nav li { .movies .alph_nav .numbers li, .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 24px; width: 22px;
height: 24px; height: 24px;
line-height: 26px; line-height: 26px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
color: #666; color: rgba(255,255,255,0.2);
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.1s ease-in-out;
text-shadow: none;
} }
.movies .alph_nav li:first-child { .movies .alph_nav .numbers li:first-child {
width: 34px; width: 43px;
} margin-left: 7px;
.movies .alph_nav li.active, .movies .alph_nav li:hover {
font-weight: bolder;
color: #fff;
} }
.movies .alph_nav li.available { .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 { .movies .alph_nav input {
padding: 6px 5px; padding: 6px 5px;
margin: 0; margin: 0 0 0 6px;
float: right; 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; var self = this;
self.data = data; self.data = data;
self.view = options.view || 'thumbs';
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
@@ -17,6 +18,13 @@ var Movie = new Class({
var self = this; var self = this;
self.el = new Element('div.movie.inlay').adopt( 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.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', { self.data_container = new Element('div.data.inlay.light', {
'tween': { 'tween': {
@@ -38,12 +46,23 @@ var Movie = new Class({
self.description = new Element('div.description', { self.description = new Element('div.description', {
'text': self.data.library.plot '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.actions = new Element('div.actions')
) )
); );
self.changeView(self.view);
self.select_checkbox_class = new Form.Check(self.select_checkbox);
// Add profile // Add profile
if(self.profile.data) if(self.profile.data)
self.profile.getTypes().each(function(type){ self.profile.getTypes().each(function(type){
@@ -57,11 +76,13 @@ var Movie = new Class({
// Add done releases // Add done releases
Array.each(self.data.releases, function(release){ Array.each(self.data.releases, function(release){
var q = self.quality.getElement('.q_'+ release.quality.identifier); var q = self.quality.getElement('.q_id'+ release.quality_id),
if(!q) status = Status.get(release.status_id);
var q = self.addQuality(release.quality_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); var q = Quality.getQuality(quality_id);
return new Element('span', { return new Element('span', {
'text': q.label, 'text': q.label,
'class': 'q_'+q.identifier 'class': 'q_'+q.identifier + ' q_id' + q.id
}).inject(self.quality); }).inject(self.quality);
}, },
@@ -97,18 +118,30 @@ var Movie = new Class({
}).pop() }).pop()
if(title) if(title)
return title.title return self.getUnprefixedTitle(title.title)
else if(titles.length > 0) else if(titles.length > 0)
return titles[0].title return self.getUnprefixedTitle(titles[0].title)
return 'Unknown movie' return 'Unknown movie'
}, },
getUnprefixedTitle: function(t){
if(t.substr(0, 4).toLowerCase() == 'the ')
t = t.substr(4) + ', The';
return t;
},
slide: function(direction, el){ slide: function(direction, el){
var self = this; var self = this;
if(direction == 'in'){ 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(); el.show();
self.data_container.tween('right', 0, -840); 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){ get: function(attr){
return this.data[attr] || this.data.library[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){ gotoIMDB: function(e){
var self = this; var self = this;
(e).stop(); (e).preventDefault();
window.open('http://www.imdb.com/title/'+self.id+'/'); window.open('http://www.imdb.com/title/'+self.id+'/');
} }
@@ -208,7 +260,7 @@ var ReleaseAction = new Class({
show: function(e){ show: function(e){
var self = this; var self = this;
(e).stop(); (e).preventDefault();
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').adopt(
@@ -226,11 +278,15 @@ var ReleaseAction = new Class({
).inject(self.release_container) ).inject(self.release_container)
Array.each(self.movie.data.releases, function(release){ Array.each(self.movie.data.releases, function(release){
var status = Status.get(release.status_id),
quality = Quality.getProfile(release.quality_id)
new Element('div', { new Element('div', {
'class': 'item ' + release.status.identifier 'class': 'item ' + status.identifier
}).adopt( }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), 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.size', {'text': (self.get(release, 'size') || 'unknown')}),
new Element('span.age', {'text': self.get(release, 'age')}), new Element('span.age', {'text': self.get(release, 'age')}),
new Element('span.score', {'text': self.get(release, 'score')}), new Element('span.score', {'text': self.get(release, 'score')}),
@@ -238,7 +294,7 @@ var ReleaseAction = new Class({
new Element('a.download.icon', { new Element('a.download.icon', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).stop(); (e).preventDefault();
self.download(release); self.download(release);
} }
} }
@@ -246,9 +302,9 @@ var ReleaseAction = new Class({
new Element('a.delete.icon', { new Element('a.delete.icon', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).stop(); (e).preventDefault();
self.del(release); self.ignore(release);
this.getParent('.item').destroy(); this.getParent('.item').toggleClass('ignored')
} }
} }
}) })
@@ -278,10 +334,10 @@ var ReleaseAction = new Class({
}); });
}, },
del: function(release){ ignore: function(release){
var self = this; var self = this;
Api.request('release.delete', { Api.request('release.ignore', {
'data': { 'data': {
'id': release.id '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 { .search_form {
display: inline-block; display: inline-block;
vertical-align: middle;
width: 25%; width: 25%;
} }
.search_form input { .search_form input {
padding-right: 25px; padding: 4px 20px 4px 4px;
padding: 4px;
margin: 0; margin: 0;
font-size: 14px; 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 { .search_form .input a {
width: 17px; width: 17px;
height: 20px; height: 20px;
display: inline-block; display: inline-block;
margin: 0 0 -5px -20px; margin: -2px 0 0 2px;
top: 4px; top: 4px;
right: 5px; right: 5px;
background: url('../images/checks.png') right -36px no-repeat; background: url('../images/sprite.png') left -37px no-repeat;
cursor: pointer; 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 { .search_form .results_container {
position: absolute; position: absolute;
background: #5c697b; background: #5c697b;
margin: 6px 0 0 -246px; margin: 6px 0 0 -230px;
width: 470px; width: 470px;
min-height: 140px; min-height: 140px;
border-radius: 3px; border-radius: 3px;
-webkit-border-radius: 3px; box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
-moz-border-radius: 3px; display: none;
box-shadow: 0 0 50px rgba(0,0,0,0.55);
} }
.search_form .spinner { .search_form.shown.filled .results_container {
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;
display: block; display: block;
position: absolute; }
width: 0px;
left: 50%; .search_form .results_container:before {
margin: -9px 0 0 110px; content: ' ';
height: 0;
position: relative;
width: 0;
border: 10px solid transparent;
border-bottom-color: #5c697b;
display: block;
top: -20px;
left: 346px;
} }
.search_form .results { .search_form .results {
max-height: 550px; max-height: 570px;
overflow-x: hidden; overflow-x: hidden;
padding: 10px 0; padding: 10px 0;
margin-top: -18px;
} }
.movie_result { .movie_result {
overflow: hidden; overflow: hidden;
min-height: 140px; height: 140px;
} }
.movie_result .options { .movie_result .options {
height: 139px; height: 140px;
border: 1px solid transparent; border: 1px solid transparent;
border-width: 1px 0; border-width: 1px 0;
border-radius: 0; border-radius: 0;
@@ -106,12 +133,11 @@
.movie_result .data { .movie_result .data {
padding: 0 15px; padding: 0 15px;
width: 440px; width: 470px;
position: relative; position: relative;
min-height: 100px; height: 140px;
top: 0; top: 0;
margin: -143px 0 0 0; margin: -140px 0 0 0;
min-height: 140px;
background: #5c697b; background: #5c697b;
cursor: pointer; cursor: pointer;
@@ -120,7 +146,7 @@
} }
.movie_result:last-child .data { border-bottom: 0; } .movie_result:last-child .data { border-bottom: 0; }
.movie_result .in_wanted, .movie_result .in_library { .movie_result .in_wanted, .movie_result .in_library {
position: absolute; position: absolute;
margin-top: 105px; margin-top: 105px;
@@ -131,16 +157,17 @@
display: inline-block; display: inline-block;
margin: 15px 3% 15px 0; margin: 15px 3% 15px 0;
vertical-align: top; vertical-align: top;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 0 3px rgba(0,0,0,0.35); box-shadow: 0 0 3px rgba(0,0,0,0.35);
} }
.movie_result .info { .movie_result .info {
width: 74%; width: 80%;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
padding: 15px 0; padding: 15px 0;
height: 120px;
overflow: hidden;
} }
.movie_result .info .tagline { .movie_result .info .tagline {
@@ -164,3 +191,7 @@
.movie_result .info h2 span:before { content: "("; } .movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { 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( self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt( new Element('div.input').adopt(
self.input = new Element('input.inlay', { self.input = new Element('input.inlay', {
'placeholder': 'Search for new movies', 'placeholder': 'Search & add a new movie',
'events': { 'events': {
'keyup': self.keyup.bind(self), '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', { new Element('a', {
'events': { 'events': {
'click': self.clear.bind(self) 'click': self.clear.bind(self)
@@ -32,24 +43,24 @@ Block.Search = new Class({
} }
} }
}).adopt( }).adopt(
new Element('div.pointer'),
self.results = new Element('div.results') 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){ clear: function(e){
var self = this; var self = this;
(e).stop(); (e).preventDefault();
self.input.set('value', ''); self.input.set('value', '');
self.input.focus() self.input.focus()
self.movies = [] self.movies = []
self.results.empty() self.results.empty()
self.el.removeClass('filled')
}, },
hideResults: function(bool){ hideResults: function(bool){
@@ -57,7 +68,7 @@ Block.Search = new Class({
if(self.hidden == bool) return; if(self.hidden == bool) return;
self.result_container[bool ? 'hide' : 'show'](); self.el[bool ? 'removeClass' : 'addClass']('shown');
if(bool){ if(bool){
History.removeEvent('change', self.hideResults.bind(self, !bool)); History.removeEvent('change', self.hideResults.bind(self, !bool));
@@ -74,16 +85,14 @@ Block.Search = new Class({
keyup: function(e){ keyup: function(e){
var self = this; var self = this;
if(['up', 'down'].indexOf(e.key) > -1){ self.el[self.q() ? 'addClass' : 'removeClass']('filled')
p('select item')
} if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
else if(self.q() != self.last_q) {
self.autocomplete() self.autocomplete()
}
}, },
autocomplete: function(delay){ autocomplete: function(){
var self = this; var self = this;
if(!self.q()){ if(!self.q()){
@@ -91,10 +100,7 @@ Block.Search = new Class({
return return
} }
self.spinner.show() self.list()
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.list.delay((delay || 300), self)
}, },
list: function(){ list: function(){
@@ -105,9 +111,14 @@ Block.Search = new Class({
var q = self.q(); var q = self.q();
var cache = self.cache[q]; var cache = self.cache[q];
self.hideResults(false) self.hideResults(false);
if(!cache){ if(!cache){
self.positionMask().fade('in');
if(!self.spinner)
self.spinner = createSpinner(self.mask);
self.api_request = Api.request('movie.search', { self.api_request = Api.request('movie.search', {
'data': { 'data': {
'q': q 'q': q
@@ -125,7 +136,7 @@ Block.Search = new Class({
fill: function(q, json){ fill: function(q, json){
var self = this; var self = this;
self.spinner.hide(); self.positionMask()
self.cache[q] = json self.cache[q] = json
self.movies = {} self.movies = {}
@@ -138,10 +149,30 @@ Block.Search = new Class({
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
}); });
if(q != self.q()) if(q != self.q())
self.list() 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){ loading: function(bool){
@@ -252,7 +283,9 @@ Block.Search.Item = new Class({
add: function(e){ add: function(e){
var self = this; var self = this;
(e).stop(); (e).preventDefault();
self.loadingMask();
Api.request('movie.add', { Api.request('movie.add', {
'data': { 'data': {
@@ -260,8 +293,6 @@ Block.Search.Item = new Class({
'title': self.title_select.get('value'), 'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value') 'profile_id': self.profile_select.get('value')
}, },
'useSpinner': true,
'spinnerTarget': self.options,
'onComplete': function(){ 'onComplete': function(){
self.options.empty(); self.options.empty();
self.options.adopt( self.options.adopt(
@@ -293,10 +324,9 @@ Block.Search.Item = new Class({
}) : null, }) : null,
self.info.in_wanted ? new Element('span.in_wanted', { self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.label '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 'text': 'Already in library: ' + self.info.in_library.label
}) : null, }) : null),
self.title_select = new Element('select', { self.title_select = new Element('select', {
'name': 'title' 'name': 'title'
}), }),
@@ -318,7 +348,7 @@ Block.Search.Item = new Class({
}).inject(self.title_select) }).inject(self.title_select)
}) })
Object.each(Quality.getActiveProfiles(), function(profile){ Quality.getActiveProfiles().each(function(profile){
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label '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(){ toElement: function(){
return this.el return this.el
} }
@@ -1,5 +1,3 @@
/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
.add_new_profile { .add_new_profile {
padding: 20px; padding: 20px;
display: block; display: block;
@@ -18,7 +16,7 @@
padding: 14px; padding: 14px;
background-position: center; background-position: center;
} }
.profile .qualities { .profile .qualities {
min-height: 80px; min-height: 80px;
} }
@@ -42,36 +40,36 @@
.profile .wait_for input { .profile .wait_for input {
margin: 0 5px !important; margin: 0 5px !important;
} }
.profile .types { .profile .types {
padding: 0; padding: 0;
margin: 0 20px 0 -4px; margin: 0 20px 0 -4px;
display: inline-block; display: inline-block;
} }
.profile .types li { .profile .types li {
padding: 3px 5px; padding: 3px 5px;
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
list-style: none; list-style: none;
} }
.profile .types li:last-child { border: 0; } .profile .types li:last-child { border: 0; }
.profile .types li > * { .profile .types li > * {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
line-height: 0; line-height: 0;
margin-right: 10px; margin-right: 10px;
} }
.profile .quality_type select { .profile .quality_type select {
width: 186px; width: 186px;
margin-left: -1px; margin-left: -1px;
} }
.profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle { .profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
visibility: hidden; visibility: hidden;
} }
.profile .types .type .handle { .profile .types .type .handle {
background: url('./handle.png') center; background: url('./handle.png') center;
display: inline-block; display: inline-block;
@@ -82,7 +80,7 @@
cursor: -webkit-grab; cursor: -webkit-grab;
margin: 0; margin: 0;
} }
.profile .types .type .delete { .profile .types .type .delete {
background-position: left center; background-position: left center;
height: 20px; height: 20px;
@@ -90,13 +88,13 @@
visibility: hidden; visibility: hidden;
cursor: pointer; cursor: pointer;
} }
.profile .types .type:hover:not(.is_empty) .delete { .profile .types .type:hover:not(.is_empty) .delete {
visibility: visible; visibility: visible;
} }
#profile_ordering { #profile_ordering {
} }
#profile_ordering ul { #profile_ordering ul {
@@ -114,19 +112,19 @@
padding: 0 5px; padding: 0 5px;
} }
#profile_ordering li:last-child { border: 0; } #profile_ordering li:last-child { border: 0; }
#profile_ordering li .check { #profile_ordering li .check {
margin: 2px 10px 0 0; margin: 2px 10px 0 0;
vertical-align: top; vertical-align: top;
} }
#profile_ordering li > span { #profile_ordering li > span {
display: inline-block; display: inline-block;
height: 20px; height: 20px;
vertical-align: top; vertical-align: top;
line-height: 20px; line-height: 20px;
} }
#profile_ordering li .handle { #profile_ordering li .handle {
background: url('./handle.png') center; background: url('./handle.png') center;
width: 20px; width: 20px;
@@ -156,7 +156,7 @@ var Profile = new Class({
'class': 'delete', 'class': 'delete',
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).stop(); (e).preventDefault();
Api.request('profile.delete', { Api.request('profile.delete', {
'data': { 'data': {
'id': self.data.id 'id': self.data.id
+5 -1
View File
@@ -16,7 +16,7 @@ log = CPLog(__name__)
class QualityPlugin(Plugin): class QualityPlugin(Plugin):
qualities = [ 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': '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': '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']}, {'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)) log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file))
return self.setCache(hash, quality) 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)): if list(set(quality.get('tags', [])) & set(words)):
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file)) log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality) 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 {
} }
.group_sizes .head { .group_sizes .head {
@@ -17,7 +15,7 @@
.group_sizes .label { .group_sizes .label {
max-width: 120px; max-width: 120px;
} }
.group_sizes .min, .group_sizes .max { .group_sizes .min, .group_sizes .max {
text-align: center; text-align: center;
width: 50px; width: 50px;
@@ -39,10 +39,10 @@ var QualityBase = new Class({
self.settings = App.getPage('Settings') self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){ self.settings.addEvent('create', function(){
var tab = self.settings.createTab('profile', { var tab = self.settings.createSubTab('profile', {
'label': 'Quality', 'label': 'Quality',
'name': 'profile' 'name': 'profile'
}); }, self.settings.tabs.searcher ,'searcher');
self.tab = tab.tab; self.tab = tab.tab;
self.content = tab.content; self.content = tab.content;
@@ -93,7 +93,7 @@ var QualityBase = new Class({
var data = data || {'id': randomString()} var data = data || {'id': randomString()}
var profile = new Profile(data) var profile = new Profile(data)
self.profiles.include(profile) self.profiles.include(profile)
return profile; return profile;
}, },
+34 -2
View File
@@ -16,8 +16,24 @@ class Release(Plugin):
def __init__(self): def __init__(self):
addEvent('release.add', self.add) addEvent('release.add', self.add)
addApiView('release.download', self.download) addApiView('release.download', self.download, docs = {
addApiView('release.delete', self.delete) '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): def add(self, group):
db = get_session() db = get_session()
@@ -95,6 +111,22 @@ class Release(Plugin):
'success': True '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): def download(self):
db = get_session() db = get_session()
@@ -24,6 +24,7 @@ rename_options = {
config = [{ config = [{
'name': 'renamer', 'name': 'renamer',
'order': 40,
'description': 'Move and rename your downloaded movies to your movie directory.', 'description': 'Move and rename your downloaded movies to your movie directory.',
'groups': [ 'groups': [
{ {
+41 -6
View File
@@ -22,7 +22,9 @@ class Renamer(Plugin):
def __init__(self): 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('renamer.scan', self.scan)
addEvent('app.load', self.scan) addEvent('app.load', self.scan)
@@ -31,7 +33,7 @@ class Renamer(Plugin):
def scanView(self): def scanView(self):
fireEvent('renamer.scan') fireEventAsync('renamer.scan')
return jsonified({ return jsonified({
'success': True 'success': True
@@ -65,6 +67,8 @@ class Renamer(Plugin):
nfo_name = self.conf('nfo_name') nfo_name = self.conf('nfo_name')
separator = self.conf('separator') separator = self.conf('separator')
db = get_session()
for group_identifier in groups: for group_identifier in groups:
group = groups[group_identifier] group = groups[group_identifier]
@@ -123,7 +127,7 @@ class Renamer(Plugin):
# Move nfo depending on settings # Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'): if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type) 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]: for current_file in group['files'][file_type]:
remove_files.append(current_file) remove_files.append(current_file)
continue continue
@@ -223,7 +227,6 @@ class Renamer(Plugin):
cd += 1 cd += 1
# Before renaming, remove the lower quality files # Before renaming, remove the lower quality files
db = get_session()
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first() library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
@@ -308,11 +311,26 @@ class Renamer(Plugin):
if isinstance(src, File): if isinstance(src, File):
src = src.path 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 # Remove matching releases
for release in remove_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 # Search for trailers etc
fireEventAsync('renamer.after', group) fireEventAsync('renamer.after', group)
@@ -378,3 +396,20 @@ class Renamer(Plugin):
def replaceDoubles(self, string): def replaceDoubles(self, string):
return string.replace(' ', ' ').replace(' .', '.') 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'), 'trailer': ('video', 'trailer'),
'nfo': ('nfo', 'nfo'), 'nfo': ('nfo', 'nfo'),
'movie': ('video', 'movie'), 'movie': ('video', 'movie'),
'movie': ('movie', 'movie_extra'), 'movie_extra': ('movie', 'movie_extra'),
'backdrop': ('image', 'backdrop'), 'backdrop': ('image', 'backdrop'),
'leftover': ('leftover', 'leftover'), 'leftover': ('leftover', 'leftover'),
} }
@@ -365,7 +365,6 @@ class Scanner(Plugin):
def getMeta(self, filename): def getMeta(self, filename):
try: try:
p = enzyme.parse(filename) p = enzyme.parse(filename)
return { return {
'video': p.video[0].codec, 'video': p.video[0].codec,
@@ -377,6 +376,8 @@ class Scanner(Plugin):
log.debug('Failed to parse meta for %s' % filename) log.debug('Failed to parse meta for %s' % filename)
except NoParserError: except NoParserError:
log.debug('No parser found for %s' % filename) log.debug('No parser found for %s' % filename)
except:
log.debug('Failed parsing %s' % filename)
return {} 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.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
sizeScore sizeScore, providerScore, duplicateScore
log = CPLog(__name__) log = CPLog(__name__)
@@ -31,4 +31,10 @@ class Score(Plugin):
except: except:
pass pass
# Provider score
score += providerScore(nzb['provider'])
# Duplicates in name
score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title'])
return score return score
+23 -2
View File
@@ -27,14 +27,14 @@ def nameScore(name, year):
score = 0 score = 0
name = name.lower() name = name.lower()
#give points for the cool stuff # give points for the cool stuff
for value in name_scores: for value in name_scores:
v = value.split(':') v = value.split(':')
add = int(v.pop()) add = int(v.pop())
if v.pop() in name: if v.pop() in name:
score = score + add score = score + add
#points if the year is correct # points if the year is correct
if str(year) in name: if str(year) in name:
score = score + 5 score = score + 5
@@ -58,3 +58,24 @@ def nameRatioScore(nzb_name, movie_name):
def sizeScore(size): def sizeScore(size):
return 0 if size else -20 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 = [{ config = [{
'name': 'searcher', 'name': 'searcher',
'order': 20,
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
+9 -3
View File
@@ -33,13 +33,19 @@ class Searcher(Plugin):
).all() ).all()
for movie in movies: for movie in movies:
movie_dict = movie.to_dict({
self.single(movie.to_dict({
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}}, 'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files':{}},
'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 # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
+8 -1
View File
@@ -20,6 +20,7 @@ class StatusPlugin(Plugin):
'wanted': 'Wanted', 'wanted': 'Wanted',
'snatched': 'Snatched', 'snatched': 'Snatched',
'deleted': 'Deleted', 'deleted': 'Deleted',
'ignored': 'Ignored',
} }
def __init__(self): def __init__(self):
@@ -29,7 +30,13 @@ class StatusPlugin(Plugin):
addEvent('status.all', self.all) addEvent('status.all', self.all)
addEvent('app.initialize', self.fill) 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): def list(self):
@@ -5,7 +5,13 @@ var StatusBase = new Class({
self.statuses = statuses; self.statuses = statuses;
} },
get: function(id){
return this.statuses.filter(function(status){
return status.id == id
}).pick()
},
}); });
window.Status = new StatusBase(); window.Status = new StatusBase();
@@ -8,7 +8,9 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'renamer', 'tab': 'renamer',
'subtab': 'subtitles',
'name': 'subtitle', 'name': 'subtitle',
'label': 'Download subtitles after rename',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
+3 -1
View File
@@ -7,8 +7,10 @@ config = [{
'name': 'trailer', 'name': 'trailer',
'groups': [ 'groups': [
{ {
'tab': 'metadata', 'tab': 'renamer',
'subtab': 'trailer',
'name': 'trailer', 'name': 'trailer',
'label': 'Download trailer after rename',
'options': [ 'options': [
{ {
'name': 'enabled', '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.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified 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 couchpotato.environment import Env
from flask.globals import request from flask.globals import request
from flask.helpers import url_for from flask.helpers import url_for
from flask.templating import render_template
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -15,14 +15,27 @@ log = CPLog(__name__)
class Userscript(Plugin): class Userscript(Plugin):
version = 2
def __init__(self): 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', self.iFrame)
addApiView('userscript.add_via_url', self.getViaUrl) addApiView('userscript.add_via_url', self.getViaUrl)
addApiView('userscript.bookmark', self.bookmark)
addEvent('userscript.get_version', self.getVersion) 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 = { params = {
'includes': fireEvent('userscript.get_includes', merge = True), 'includes': fireEvent('userscript.get_includes', merge = True),
@@ -42,14 +55,14 @@ class Userscript(Plugin):
versions = fireEvent('userscript.get_provider_version') versions = fireEvent('userscript.get_provider_version')
version = 0 version = self.version
for v in versions: for v in versions:
version += v version += v
return version return version
def iFrame(self): def iFrame(self):
return index() return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
def getViaUrl(self): def getViaUrl(self):
@@ -62,34 +62,54 @@ var UserscriptSettingTab = new Class({
self.settings = App.getPage('Settings') self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){ self.settings.addEvent('create', function(){
var tab = self.settings.createTab('userscript', {
'label': 'Userscript', // See if userscript can be installed
'name': 'userscript' 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(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 )){ 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}); 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== // ==UserScript==
// @name CouchPotato UserScript // @name CouchPotato UserScript
// @description Add movies like a real CouchPotato // @description Add movies like a real CouchPotato
// @version {{version}} // @version {{version}}
// @match {{host}}* // @match {{host}}*
{% for include in includes %} {% for include in includes %}
@@ -57,7 +57,7 @@ if (typeof GM_addStyle == 'undefined'){
// Styles // Styles
GM_addStyle('\ 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: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#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; } \ #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 osd = function(){
var navbar, newElement; var navbar, newElement;
var createApiUrl = function(url){
return host + api + "?url=" + escape(url)
};
var iframe = create('iframe', { var iframe = create('iframe', {
'src': host + api + "?url=" + escape(document.location.href), 'src': createApiUrl(document.location.href),
'frameborder': 0, 'frameborder': 0,
'scrolling': 'no' 'scrolling': 'no'
}); });
@@ -81,32 +85,49 @@ var osd = function(){
var popup = create('div', { var popup = create('div', {
'id': 'cp_popup' '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', { var add_button = create('a', {
'innerHTML': '<img src="' + cp_icon + '" />', 'innerHTML': '<img src="' + cp_icon + '" />',
'id': 'add_to', 'id': 'add_to',
'onclick': function(){ 'onclick': onclick
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)
}
}); });
popup.appendChild(add_button); popup.appendChild(add_button);
document.body.parentNode.insertBefore(popup, document.body); document.body.parentNode.insertBefore(popup, document.body);
// Auto fold open
if(document.body.getAttribute('cp_auto_open'))
onclick()
}; };
var setVersion = function(){ var setVersion = function(){
document.body.setAttribute('data-userscript_version', version) document.body.setAttribute('data-userscript_version', version)
}; };
if(document.location.href.indexOf(host) == -1) if(document.location.href.indexOf(host) == -1)
osd(); osd();
else else
setVersion(); setVersion();
@@ -1,5 +1,3 @@
/* @override http://127.0.0.1:5000/static/wizard/wizard.css */
.page.wizard h1 { .page.wizard h1 {
padding: 10px 30px; padding: 10px 30px;
margin: 0; margin: 0;
@@ -33,14 +31,14 @@
margin: 0; margin: 0;
display: block; display: block;
} }
.page.wizard .tabs li { .page.wizard .tabs li {
display: inline-block; display: inline-block;
} }
.page.wizard .tabs li a { .page.wizard .tabs li a {
padding: 20px 30px; padding: 20px 30px;
} }
.page.wizard .tab_wrapper .pointer { .page.wizard .tab_wrapper .pointer {
border-right: 10px solid transparent; border-right: 10px solid transparent;
border-left: 10px solid transparent; border-left: 10px solid transparent;
@@ -49,7 +47,7 @@
position: absolute; position: absolute;
top: 60px; top: 60px;
} }
.page.wizard .tab_content { .page.wizard .tab_content {
margin: 20px 0 160px; margin: 20px 0 160px;
} }
@@ -29,13 +29,13 @@ Page.Wizard = new Class({
}, },
'finish': { 'finish': {
'title': 'Finish Up', '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( 'content': new Element('div').adopt(
new Element('a.button.green', { new Element('a.button.green', {
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!', 'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).stop(); (e).preventDefault();
Api.request('settings.save', { Api.request('settings.save', {
'data': { 'data': {
'section': 'core', 'section': 'core',
@@ -115,7 +115,14 @@ Page.Wizard = new Class({
if(tab_navigation && group_container){ if(tab_navigation && group_container){
tab_navigation.inject(tabs); // Tab navigation tab_navigation.inject(tabs); // Tab navigation
self.el.getElement('.tab_'+group).inject(group_container); // Tab content 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 { else {
new Element('li.t_'+group).adopt( new Element('li.t_'+group).adopt(
@@ -161,7 +168,7 @@ Page.Wizard = new Class({
if(nr == 0) if(nr == 0)
func(); func();
var ss = new ScrollSpy( { var ss = new ScrollSpy( {
min: function(){ min: function(){
var c = g.getCoordinates(); var c = g.getCoordinates();
@@ -3,21 +3,4 @@ from .main import CP
def start(): def start():
return CP() return CP()
config = [{ 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',
},
],
},
],
}]
+1 -1
View File
@@ -102,4 +102,4 @@ class YarrProvider(Provider):
return [self.cat_backup_id] return [self.cat_backup_id]
def found(self, new): 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.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import json
import os import os
import shutil import shutil
import traceback import traceback
@@ -31,11 +30,7 @@ class MetaDataBase(Plugin):
root = self.getRootName(release) root = self.getRootName(release)
try: movie_info = release['library'].get('info')
movie_info = json.loads(release['library'].get('info'))
except:
log.error('Failed to parse movie info: %s' % traceback.format_exc())
movie_info = {}
for file_type in ['nfo', 'thumbnail', 'fanart']: for file_type in ['nfo', 'thumbnail', 'fanart']:
try: try:
@@ -7,7 +7,8 @@ config = [{
'name': 'mediabrowser', 'name': 'mediabrowser',
'groups': [ 'groups': [
{ {
'tab': 'metadata', 'tab': 'renamer',
'subtab': 'metadata',
'name': 'mediabrowser_metadata', 'name': 'mediabrowser_metadata',
'label': 'MediaBrowser', 'label': 'MediaBrowser',
'description': 'Enable metadata MediaBrowser can understand', 'description': 'Enable metadata MediaBrowser can understand',
@@ -7,7 +7,8 @@ config = [{
'name': 'sonyps3', 'name': 'sonyps3',
'groups': [ 'groups': [
{ {
'tab': 'metadata', 'tab': 'renamer',
'subtab': 'metadata',
'name': 'sonyps3_metadata', 'name': 'sonyps3_metadata',
'label': 'Sony PS3', 'label': 'Sony PS3',
'description': 'Enable metadata your Playstation 3 can understand', 'description': 'Enable metadata your Playstation 3 can understand',
@@ -7,7 +7,8 @@ config = [{
'name': 'wdtv', 'name': 'wdtv',
'groups': [ 'groups': [
{ {
'tab': 'metadata', 'tab': 'renamer',
'subtab': 'metadata',
'name': 'wdtv_metadata', 'name': 'wdtv_metadata',
'label': 'WDTV', 'label': 'WDTV',
'description': 'Enable metadata WDTV can understand', 'description': 'Enable metadata WDTV can understand',
@@ -7,7 +7,8 @@ config = [{
'name': 'xbmc', 'name': 'xbmc',
'groups': [ 'groups': [
{ {
'tab': 'metadata', 'tab': 'renamer',
'subtab': 'metadata',
'name': 'xbmc_metadata', 'name': 'xbmc_metadata',
'label': 'XBMC', 'label': 'XBMC',
'description': 'Enable metadata XBMC can understand', 'description': 'Enable metadata XBMC can understand',
@@ -48,7 +48,7 @@ class XBMC(MetaDataBase):
pass pass
# Other values # 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: for type in types:
if ':' in type: if ':' in type:
@@ -73,7 +73,7 @@ class XBMC(MetaDataBase):
votes.text = str(v) votes.text = str(v)
break break
except: 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 # Genre
for genre in movie_info.get('genres', []): for genre in movie_info.get('genres', []):
@@ -87,9 +87,9 @@ class XBMC(MetaDataBase):
name.text = toUnicode(actor) name.text = toUnicode(actor)
# Directors # Directors
for director in movie_info.get('directors', []): for director_name in movie_info.get('directors', []):
director = SubElement(nfoxml, 'director') director = SubElement(nfoxml, 'director')
director.text = toUnicode(director) director.text = toUnicode(director_name)
# Writers # Writers
for writer in movie_info.get('writers', []): for writer in movie_info.get('writers', []):
@@ -14,7 +14,7 @@ class IMDBAPI(MovieProvider):
urls = { urls = {
'search': 'http://www.imdbapi.com/?%s', '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 http_time_between_calls = 0
@@ -32,8 +32,11 @@ class IMDBAPI(MovieProvider):
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') if result.get('titles') and len(result.get('titles')) > 0:
return [result] log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return [result]
return []
return [] return []
@@ -44,8 +47,9 @@ class IMDBAPI(MovieProvider):
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') if result.get('titles') and len(result.get('titles')) > 0:
return result log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
return result
return {} return {}
@@ -57,11 +61,19 @@ class IMDBAPI(MovieProvider):
if isinstance(movie, (str, unicode)): if isinstance(movie, (str, unicode)):
movie = json.loads(movie) 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 = { movie_data = {
'titles': [movie.get('Title', '')], 'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''), 'original_title': movie.get('Title', ''),
'images': { 'images': {
'poster': [movie.get('Poster', '')], 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
}, },
'rating': { 'rating': {
'imdb': (tryFloat(movie.get('Rating', 0)), tryInt(movie.get('Votes', ''))), 'imdb': (tryFloat(movie.get('Rating', 0)), tryInt(movie.get('Votes', ''))),
@@ -10,7 +10,7 @@ config = [{
'tab': 'providers', 'tab': 'providers',
'name': 'tmdb', 'name': 'tmdb',
'label': 'TheMovieDB', 'label': 'TheMovieDB',
'advanced': True, 'hidden': True,
'description': 'Used for all calls to TheMovieDB.', 'description': 'Used for all calls to TheMovieDB.',
'options': [ 'options': [
{ {
@@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider):
def __init__(self): def __init__(self):
addEvent('movie.by_hash', self.byHash) addEvent('movie.by_hash', self.byHash)
addEvent('movie.search', self.search) addEvent('movie.search', self.search, priority = 1)
addEvent('movie.info', self.getInfo) addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.info_by_tmdb', self.getInfoByTMDBId) addEvent('movie.info_by_tmdb', self.getInfoByTMDBId)
# Use base wrapper # Use base wrapper
@@ -131,7 +131,7 @@ class TheMovieDb(MovieProvider):
# Images # Images
poster = self.getImage(movie, type = 'poster', size = 'cover') 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') poster_original = self.getImage(movie, type = 'poster', size = 'original')
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original') backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
@@ -152,7 +152,7 @@ class TheMovieDb(MovieProvider):
'original_title': movie.get('original_name'), 'original_title': movie.get('original_name'),
'images': { 'images': {
'poster': [poster] if poster else [], 'poster': [poster] if poster else [],
'backdrop': [backdrop] if backdrop else [], #'backdrop': [backdrop] if backdrop else [],
'poster_original': [poster_original] if poster_original else [], 'poster_original': [poster_original] if poster_original else [],
'backdrop_original': [backdrop_original] if backdrop_original else [], 'backdrop_original': [backdrop_original] if backdrop_original else [],
}, },
@@ -7,7 +7,8 @@ config = [{
'name': 'moovee', 'name': 'moovee',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.moovee', 'name': '#alt.binaries.moovee',
'description': 'SD movies only', 'description': 'SD movies only',
'options': [ 'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'mysterbin', 'name': 'mysterbin',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'Mysterbin', 'name': 'Mysterbin',
'description': '', 'description': '',
'options': [ 'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'newzbin', 'name': 'newzbin',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'newzbin', 'name': 'newzbin',
'wizard': True, 'wizard': True,
'options': [ 'options': [
@@ -26,6 +26,7 @@ class Newzbin(NZBProvider, RSS):
1024: ['r5'], 1024: ['r5'],
} }
cat_ids = [ cat_ids = [
([262144], ['bd50']),
([2097152], ['1080p']), ([2097152], ['1080p']),
([524288], ['720p']), ([524288], ['720p']),
([262144], ['brrip']), ([262144], ['brrip']),
@@ -7,7 +7,8 @@ config = [{
'name': 'newznab', 'name': 'newznab',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'newznab', 'name': 'newznab',
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>', 'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>',
'wizard': True, 'wizard': True,
@@ -20,8 +20,10 @@ class Newznab(NZBProvider, RSS):
} }
cat_ids = [ cat_ids = [
([2010], ['dvdr']),
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
([2040], ['720p', '1080p']), ([2040], ['720p', '1080p']),
([2050], ['bd50']),
] ]
cat_backup_id = 2000 cat_backup_id = 2000
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbclub', 'name': 'nzbclub',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'NZBClub', 'name': 'NZBClub',
'description': '', 'description': '',
'options': [ 'options': [
@@ -59,9 +59,7 @@ class NZBClub(NZBProvider, RSS):
size = enclosure['length'] size = enclosure['length']
date = self.getTextElement(nzb, "pubDate") date = self.getTextElement(nzb, "pubDate")
description = '' description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000))
if 'nfo files' in self.getTextElement(nzb, "description"):
description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000))
new = { new = {
'id': nzbclub_id, 'id': nzbclub_id,
@@ -71,12 +69,16 @@ class NZBClub(NZBProvider, RSS):
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': tryInt(size) / 1024 / 1024, 'size': tryInt(size) / 1024 / 1024,
'url': enclosure['url'], 'url': enclosure['url'],
'download': enclosure['url'], 'download': enclosure['url'].replace(' ', '_'),
'detail_url': self.getTextElement(nzb, "link"), 'detail_url': self.getTextElement(nzb, "link"),
'description': description, 'description': description,
} }
new['score'] = fireEvent('score.calculate', new, movie, single = True) 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', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single_category = False, single = True)
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbindex', 'name': 'nzbindex',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbindex', 'name': 'nzbindex',
'description': 'Free provider, but less accurate.', 'description': 'Free provider, but less accurate.',
'options': [ 'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbmatrix', 'name': 'nzbmatrix',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbmatrix', 'name': 'nzbmatrix',
'label': 'NZBMatrix', 'label': 'NZBMatrix',
'wizard': True, 'wizard': True,
@@ -7,7 +7,8 @@ config = [{
'name': 'nzbs', 'name': 'nzbs',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'nzbs', '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>.', '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, 'wizard': True,
@@ -7,7 +7,8 @@ config = [{
'name': 'x264', 'name': 'x264',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.hdtv.x264', 'name': '#alt.binaries.hdtv.x264',
'description': 'HD movies only', 'description': 'HD movies only',
'options': [ 'options': [
@@ -7,7 +7,8 @@ config = [{
'name': 'kickasstorrents', 'name': 'kickasstorrents',
'groups': [ 'groups': [
{ {
'tab': 'providers', 'tab': 'searcher',
'subtab': 'providers',
'name': 'KickAssTorrents', 'name': 'KickAssTorrents',
'options': [ 'options': [
{ {
@@ -11,7 +11,7 @@ class AlloCine(UserscriptBase):
return 'Url isn\'t from a movie' return 'Url isn\'t from a movie'
try: try:
data = self.urlopen(url) data = self.getUrl(url)
except: except:
return return
@@ -9,7 +9,7 @@ class AppleTrailers(UserscriptBase):
def getMovie(self, url): def getMovie(self, url):
try: try:
data = self.urlopen(url) data = self.getUrl(url)
except: except:
return return
@@ -1,5 +1,6 @@
from couchpotato.core.event import addEvent, fireEvent 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.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from urlparse import urlparse from urlparse import urlparse
@@ -42,9 +43,12 @@ class UserscriptBase(Plugin):
return return
def getUrl(self, url):
return self.getCache(md5(simplifyString(url)), url = url)
def getMovie(self, url): def getMovie(self, url):
try: try:
data = self.urlopen(url) data = self.getUrl(url)
except: except:
data = '' data = ''
return self.getInfo(getImdb(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): def getMovie(self, url):
match = re.search('(?P<id>\d+)', url) match = re.search('(?P<id>\d+)', url)
movie = fireEvent('movie.info_by_tmdb', id = match.group('id'), merge = True) 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): def __init__(self):
addApiView('settings', self.view) addApiView('settings', self.view, docs = {
addApiView('settings.save', self.saveView) '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): def setFile(self, config_file):
self.file = config_file self.file = config_file
@@ -128,7 +161,6 @@ class Settings(object):
if not self.options.get(section_name): if not self.options.get(section_name):
self.options[section_name] = options self.options[section_name] = options
else: else:
options['groups'] = self.options[section_name].get('groups') + options.get('groups')
self.options[section_name] = mergeDicts(self.options[section_name], options) self.options[section_name] = mergeDicts(self.options[section_name], options)
def getOptions(self): def getOptions(self):
@@ -136,7 +168,6 @@ class Settings(object):
def view(self): def view(self):
return jsonified({ return jsonified({
'options': self.getOptions(), 'options': self.getOptions(),
'values': self.getValues() '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.entity import Entity
from elixir.fields import Field from elixir.fields import Field
from elixir.options import options_defaults, using_options from elixir.options import options_defaults, using_options
from elixir.relationships import ManyToMany, OneToMany, ManyToOne from elixir.relationships import ManyToMany, OneToMany, ManyToOne
from libs.elixir.relationships import OneToOne
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \ from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String String, TypeDecorator
import json
import time
options_defaults["shortnames"] = True options_defaults["shortnames"] = True
@@ -15,13 +17,29 @@ options_defaults["shortnames"] = True
# http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata # http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata
__session__ = None __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): class Movie(Entity):
"""Movie Resource a movie could have multiple releases """Movie Resource a movie could have multiple releases
The files belonging to the movie object are global for the whole movie The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails""" such as trailers, nfo, thumbnails"""
last_edit = Field(Integer) last_edit = Field(Integer, default = lambda: int(time.time()))
library = ManyToOne('Library') library = ManyToOne('Library')
status = ManyToOne('Status') status = ManyToOne('Status')
@@ -34,12 +52,11 @@ class Library(Entity):
"""""" """"""
year = Field(Integer) year = Field(Integer)
identifier = Field(String(20)) identifier = Field(String(20), index = True)
rating = Field(Float)
plot = Field(UnicodeText) plot = Field(UnicodeText)
tagline = Field(UnicodeText(255)) tagline = Field(UnicodeText(255))
info = Field(UnicodeText) info = Field(JsonType)
status = ManyToOne('Status') status = ManyToOne('Status')
movies = OneToMany('Movie') movies = OneToMany('Movie')
@@ -52,8 +69,8 @@ class LibraryTitle(Entity):
using_options(order_by = '-default') using_options(order_by = '-default')
title = Field(Unicode) title = Field(Unicode)
simple_title = Field(Unicode) simple_title = Field(Unicode, index = True)
default = Field(Boolean) default = Field(Boolean, index = True)
language = OneToMany('Language') language = OneToMany('Language')
libraries = ManyToOne('Library') libraries = ManyToOne('Library')
@@ -62,7 +79,7 @@ class LibraryTitle(Entity):
class Language(Entity): class Language(Entity):
"""""" """"""
identifier = Field(String(20)) identifier = Field(String(20), index = True)
label = Field(Unicode) label = Field(Unicode)
titles = ManyToOne('LibraryTitle') titles = ManyToOne('LibraryTitle')
@@ -72,7 +89,7 @@ class Release(Entity):
"""Logically groups all files that belong to a certain release, such as """Logically groups all files that belong to a certain release, such as
parts of a movie, subtitles.""" parts of a movie, subtitles."""
identifier = Field(String(100)) identifier = Field(String(100), index = True)
movie = ManyToOne('Movie') movie = ManyToOne('Movie')
status = ManyToOne('Status') status = ManyToOne('Status')
@@ -85,7 +102,7 @@ class Release(Entity):
class ReleaseInfo(Entity): class ReleaseInfo(Entity):
"""Properties that can be bound to a file for off-line usage""" """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) value = Field(Unicode(255), nullable = False)
release = ManyToOne('Release') release = ManyToOne('Release')
@@ -107,7 +124,7 @@ class Quality(Entity):
identifier = Field(String(20), unique = True) identifier = Field(String(20), unique = True)
label = Field(Unicode(20)) label = Field(Unicode(20))
order = Field(Integer) order = Field(Integer, index = True)
size_min = Field(Integer) size_min = Field(Integer)
size_max = Field(Integer) size_max = Field(Integer)
@@ -121,7 +138,7 @@ class Profile(Entity):
using_options(order_by = 'order') using_options(order_by = 'order')
label = Field(Unicode(50)) label = Field(Unicode(50))
order = Field(Integer) order = Field(Integer, index = True)
core = Field(Boolean) core = Field(Boolean)
hide = Field(Boolean) hide = Field(Boolean)
@@ -133,7 +150,7 @@ class ProfileType(Entity):
"""""" """"""
using_options(order_by = 'order') using_options(order_by = 'order')
order = Field(Integer) order = Field(Integer, index = True)
finish = Field(Boolean) finish = Field(Boolean)
wait_for = Field(Integer) wait_for = Field(Integer)
@@ -170,7 +187,7 @@ class FileType(Entity):
class FileProperty(Entity): class FileProperty(Entity):
"""Properties that can be bound to a file for off-line usage""" """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) value = Field(Unicode(255), nullable = False)
file = ManyToOne('File') file = ManyToOne('File')
@@ -180,8 +197,8 @@ class History(Entity):
"""History of actions that are connected to a certain release, """History of actions that are connected to a certain release,
such as, renamed to, downloaded, deleted, download subtitles etc""" such as, renamed to, downloaded, deleted, download subtitles etc"""
added = Field(Integer) added = Field(Integer, default = lambda: int(time.time()))
message = Field(UnicodeText()) message = Field(UnicodeText)
type = Field(Unicode(50)) type = Field(Unicode(50))
release = ManyToOne('Release') release = ManyToOne('Release')
@@ -196,6 +213,15 @@ class RenameHistory(Entity):
file = ManyToOne('File') 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): class Folder(Entity):
"""Renamer destination folders.""" """Renamer destination folders."""
+18 -2
View File
@@ -1,6 +1,7 @@
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.loader import Loader from couchpotato.core.loader import Loader
from couchpotato.core.settings import Settings from couchpotato.core.settings import Settings
import os
class Env(object): class Env(object):
@@ -61,8 +62,12 @@ class Env(object):
return s return s
@staticmethod @staticmethod
def getPermission(type): def getPermission(setting_type):
return int(Env.get('settings').get('permission_%s' % type, default = 0777)) perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777')
if perm[0] == '0':
return int(perm, 8)
else:
return int(perm)
@staticmethod @staticmethod
def fireEvent(*args, **kwargs): def fireEvent(*args, **kwargs):
@@ -71,3 +76,14 @@ class Env(object):
@staticmethod @staticmethod
def addEvent(*args, **kwargs): def addEvent(*args, **kwargs):
return 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 # 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 initialize = True
try: db = Env.get('db_path')
current_db_version = db_version(db, repo) if os.path.isfile(db.replace('sqlite:///', '')):
initialize = False 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: from migrate.versioning.api import version_control, db_version, version, upgrade
log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version)) repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
upgrade(db, repo) 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 # Configure Database
from couchpotato.core.settings.model import setup 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') if fire_load: fireEventAsync('app.load')
# Go go go! # 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
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff. Show more