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:
Regular → Executable
+124
-110
@@ -4,124 +4,138 @@ from os.path import dirname
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
|
||||
|
||||
# Root path
|
||||
base_path = dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
# Insert local directories into path
|
||||
sys.path.insert(0, os.path.join(base_path, 'libs'))
|
||||
|
||||
|
||||
from couchpotato.environment import Env
|
||||
from couchpotato.core.helpers.variable import getDataDir
|
||||
|
||||
|
||||
class Loader(object):
|
||||
|
||||
do_restart = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Get options via arg
|
||||
from couchpotato.runner import getOptions
|
||||
self.options = getOptions(base_path, sys.argv[1:])
|
||||
|
||||
# Load settings
|
||||
settings = Env.get('settings')
|
||||
settings.setFile(self.options.config_file)
|
||||
|
||||
# Create data dir if needed
|
||||
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
|
||||
if self.data_dir == '':
|
||||
self.data_dir = getDataDir()
|
||||
|
||||
if not os.path.isdir(self.data_dir):
|
||||
os.makedirs(self.data_dir)
|
||||
|
||||
# Create logging dir
|
||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||
if not os.path.isdir(self.log_dir):
|
||||
os.mkdir(self.log_dir)
|
||||
|
||||
# Logging
|
||||
from couchpotato.core.logger import CPLog
|
||||
self.log = CPLog(__name__)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S')
|
||||
hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10)
|
||||
hdlr.setLevel(logging.CRITICAL)
|
||||
hdlr.setFormatter(formatter)
|
||||
self.log.logger.addHandler(hdlr)
|
||||
|
||||
def addSignals(self):
|
||||
|
||||
signal.signal(signal.SIGINT, self.onExit)
|
||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
addEvent('app.after_shutdown', self.afterShutdown)
|
||||
|
||||
def afterShutdown(self, restart):
|
||||
self.do_restart = restart
|
||||
|
||||
def onExit(self, signal, frame):
|
||||
from couchpotato.core.event import fireEvent
|
||||
fireEvent('app.crappy_shutdown', single = True)
|
||||
|
||||
def run(self):
|
||||
|
||||
self.addSignals()
|
||||
|
||||
from couchpotato.runner import runCouchPotato
|
||||
runCouchPotato(self.options, base_path, sys.argv[1:], data_dir = self.data_dir, log_dir = self.log_dir, Env = Env)
|
||||
|
||||
if self.do_restart:
|
||||
self.restart()
|
||||
|
||||
def restart(self):
|
||||
try:
|
||||
# remove old pidfile first
|
||||
try:
|
||||
if self.runAsDaemon():
|
||||
self.daemon.delpid()
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
|
||||
subprocess.Popen(args)
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
def daemonize(self):
|
||||
|
||||
if self.runAsDaemon():
|
||||
try:
|
||||
from daemon import Daemon
|
||||
self.daemon = Daemon(self.options.pid_file)
|
||||
self.daemon.daemonize()
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
def runAsDaemon(self):
|
||||
return self.options.daemon and self.options.pid_file
|
||||
|
||||
|
||||
|
||||
do_restart = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Get options via arg
|
||||
from couchpotato.runner import getOptions
|
||||
self.options = getOptions(base_path, sys.argv[1:])
|
||||
|
||||
# Load settings
|
||||
settings = Env.get('settings')
|
||||
settings.setFile(self.options.config_file)
|
||||
|
||||
# Create data dir if needed
|
||||
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
|
||||
if self.data_dir == '':
|
||||
self.data_dir = getDataDir()
|
||||
|
||||
if not os.path.isdir(self.data_dir):
|
||||
os.makedirs(self.data_dir)
|
||||
|
||||
# Create logging dir
|
||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||
if not os.path.isdir(self.log_dir):
|
||||
os.mkdir(self.log_dir)
|
||||
|
||||
# Logging
|
||||
from couchpotato.core.logger import CPLog
|
||||
self.log = CPLog(__name__)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S')
|
||||
hdlr = handlers.RotatingFileHandler(os.path.join(self.log_dir, 'error.log'), 'a', 500000, 10)
|
||||
hdlr.setLevel(logging.CRITICAL)
|
||||
hdlr.setFormatter(formatter)
|
||||
self.log.logger.addHandler(hdlr)
|
||||
|
||||
def addSignals(self):
|
||||
|
||||
signal.signal(signal.SIGINT, self.onExit)
|
||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||
|
||||
from couchpotato.core.event import addEvent
|
||||
addEvent('app.after_shutdown', self.afterShutdown)
|
||||
|
||||
def afterShutdown(self, restart):
|
||||
self.do_restart = restart
|
||||
|
||||
def onExit(self, signal, frame):
|
||||
from couchpotato.core.event import fireEvent
|
||||
fireEvent('app.crappy_shutdown', single = True)
|
||||
|
||||
def run(self):
|
||||
|
||||
self.addSignals()
|
||||
|
||||
from couchpotato.runner import runCouchPotato
|
||||
runCouchPotato(self.options, base_path, sys.argv[1:], data_dir = self.data_dir, log_dir = self.log_dir, Env = Env)
|
||||
|
||||
if self.do_restart:
|
||||
self.restart()
|
||||
|
||||
def restart(self):
|
||||
try:
|
||||
# remove old pidfile first
|
||||
try:
|
||||
if self.runAsDaemon():
|
||||
try: self.daemon.stop()
|
||||
except: pass
|
||||
self.daemon.delpid()
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
|
||||
subprocess.Popen(args)
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
def daemonize(self):
|
||||
|
||||
if self.runAsDaemon():
|
||||
try:
|
||||
from daemon import Daemon
|
||||
self.daemon = Daemon(self.options.pid_file)
|
||||
self.daemon.daemonize()
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
def runAsDaemon(self):
|
||||
return self.options.daemon and self.options.pid_file
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
l = Loader()
|
||||
l.daemonize()
|
||||
l.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as (nr, msg):
|
||||
if nr != 4:
|
||||
try:
|
||||
l.log.critical(traceback.format_exc())
|
||||
except:
|
||||
print traceback.format_exc()
|
||||
try:
|
||||
l = Loader()
|
||||
l.daemonize()
|
||||
l.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit:
|
||||
raise
|
||||
except socket.error as (nr, msg):
|
||||
# log when socket receives SIGINT, but continue.
|
||||
# previous code would have skipped over other types of IO errors too.
|
||||
if nr != 4:
|
||||
try:
|
||||
l.log.critical(traceback.format_exc())
|
||||
except:
|
||||
print traceback.format_exc()
|
||||
raise
|
||||
except:
|
||||
try:
|
||||
# if this fails we will have two tracebacks
|
||||
# one for failing to log, and one for the exception that got us here.
|
||||
l.log.critical(traceback.format_exc())
|
||||
except:
|
||||
print traceback.format_exc()
|
||||
raise
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.api import api_docs, api_docs_missing
|
||||
from couchpotato.core.auth import requires_auth
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -35,6 +36,21 @@ def addView(route, func, static = False):
|
||||
def index():
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
|
||||
""" Api view """
|
||||
@web.route('docs/')
|
||||
@requires_auth
|
||||
def apiDocs():
|
||||
from couchpotato import app
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:].replace('::', '.')]
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
del api_docs_missing['']
|
||||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
index_url = url_for('web.index')
|
||||
|
||||
+13
-12
@@ -1,21 +1,22 @@
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
def addApiView(route, func, static = False):
|
||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '-') if route else 'index', view_func = func)
|
||||
def addApiView(route, func, static = False, docs = None):
|
||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func)
|
||||
if docs:
|
||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
""" Api view """
|
||||
def index():
|
||||
from couchpotato import app
|
||||
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:]]
|
||||
|
||||
return jsonified({'routes': routes})
|
||||
index_url = url_for('web.index')
|
||||
return redirect(index_url + 'docs/')
|
||||
|
||||
addApiView('', index)
|
||||
addApiView('default', index)
|
||||
|
||||
@@ -6,6 +6,7 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'core',
|
||||
'order': 1,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'general',
|
||||
@@ -54,7 +55,7 @@ config = [{
|
||||
'name': 'api_key',
|
||||
'default': uuid4().hex,
|
||||
'readonly': 1,
|
||||
'description': "This is top-secret! Don't share this!",
|
||||
'description': 'Let 3rd party app do stuff. <a target="_self" href="/docs/">Docs</a>',
|
||||
},
|
||||
{
|
||||
'name': 'debug',
|
||||
@@ -80,13 +81,13 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'permission_folder',
|
||||
'default': 0755,
|
||||
'default': '0755',
|
||||
'label': 'Folder CHMOD',
|
||||
'description': 'Permission (decimal) for creating/copying folders. 0755 => 593, 0777 => 511',
|
||||
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
|
||||
},
|
||||
{
|
||||
'name': 'permission_file',
|
||||
'default': 0755,
|
||||
'default': '0755',
|
||||
'label': 'File CHMOD',
|
||||
'description': 'Same as Folder CHMOD but for files',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -12,21 +12,26 @@ import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
|
||||
if os.name == 'nt':
|
||||
import getppid
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Core(Plugin):
|
||||
|
||||
ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
|
||||
shutdown_started = False
|
||||
|
||||
def __init__(self):
|
||||
addApiView('app.shutdown', self.shutdown)
|
||||
addApiView('app.restart', self.restart)
|
||||
addApiView('app.available', self.available)
|
||||
addApiView('app.shutdown', self.shutdown, docs = {
|
||||
'desc': 'Shutdown the app.',
|
||||
'return': {'type': 'string: shutdown'}
|
||||
})
|
||||
addApiView('app.restart', self.restart, docs = {
|
||||
'desc': 'Restart the app.',
|
||||
'return': {'type': 'string: restart'}
|
||||
})
|
||||
addApiView('app.available', self.available, docs = {
|
||||
'desc': 'Check if app available.'
|
||||
})
|
||||
|
||||
addEvent('app.crappy_shutdown', self.crappyShutdown)
|
||||
addEvent('app.crappy_restart', self.crappyRestart)
|
||||
@@ -51,6 +56,9 @@ class Core(Plugin):
|
||||
})
|
||||
|
||||
def crappyShutdown(self):
|
||||
if self.shutdown_started:
|
||||
return
|
||||
|
||||
try:
|
||||
self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
|
||||
return True
|
||||
@@ -59,6 +67,9 @@ class Core(Plugin):
|
||||
return False
|
||||
|
||||
def crappyRestart(self):
|
||||
if self.shutdown_started:
|
||||
return
|
||||
|
||||
try:
|
||||
self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False)
|
||||
return True
|
||||
@@ -77,6 +88,7 @@ class Core(Plugin):
|
||||
def initShutdown(self, restart = False):
|
||||
if self.shutdown_started:
|
||||
log.info('Already shutting down')
|
||||
return
|
||||
|
||||
log.info('Shutting down' if not restart else 'Restarting')
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class Scheduler(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.getLogger('apscheduler').setLevel(logging.WARNING)
|
||||
logging.getLogger('apscheduler').setLevel(logging.ERROR)
|
||||
|
||||
addEvent('schedule.cron', self.cron)
|
||||
addEvent('schedule.interval', self.interval)
|
||||
|
||||
@@ -5,6 +5,7 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from git.repository import LocalRepository
|
||||
from datetime import datetime
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
@@ -29,18 +30,34 @@ class Updater(Plugin):
|
||||
|
||||
addEvent('app.load', self.check)
|
||||
|
||||
addApiView('updater.info', self.getInfo)
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
'desc': 'Get updater information',
|
||||
'return': {
|
||||
'type': 'object',
|
||||
'example': """{
|
||||
'repo_name': "Name of used repository",
|
||||
'last_check': "last checked for update",
|
||||
'update_version': "available update version or empty",
|
||||
'version': current_cp_version
|
||||
}"""}
|
||||
})
|
||||
addApiView('updater.update', self.doUpdateView)
|
||||
addApiView('updater.check', self.checkView)
|
||||
addApiView('updater.check', self.checkView, docs = {
|
||||
'desc': 'Check for available update',
|
||||
'return': {'type': 'see updater.info'}
|
||||
})
|
||||
|
||||
def getInfo(self):
|
||||
|
||||
return jsonified({
|
||||
return jsonified(self.info())
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
'repo_name': self.repo_name,
|
||||
'last_check': self.last_check,
|
||||
'update_version': self.update_version,
|
||||
'version': self.getVersion()
|
||||
})
|
||||
}
|
||||
|
||||
def getVersion(self):
|
||||
|
||||
@@ -78,14 +95,14 @@ class Updater(Plugin):
|
||||
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8]))
|
||||
|
||||
if local.getDate() < remote.getDate():
|
||||
self.update_version = {
|
||||
'hash': remote.hash[:8],
|
||||
'date': remote.getDate(),
|
||||
}
|
||||
if self.conf('automatic') and not self.update_failed:
|
||||
if self.doUpdate():
|
||||
fireEventAsync('app.crappy_restart')
|
||||
else:
|
||||
self.update_version = {
|
||||
'hash': remote.hash[:8],
|
||||
'date': remote.getDate(),
|
||||
}
|
||||
if self.conf('notification'):
|
||||
fireEvent('updater.available', message = 'A new update is available', data = self.getVersion())
|
||||
|
||||
@@ -106,11 +123,16 @@ class Updater(Plugin):
|
||||
self.repo.saveStash()
|
||||
|
||||
log.info('Updating to latest version')
|
||||
info = self.info()
|
||||
self.repo.pull()
|
||||
|
||||
# Delete leftover .pyc files
|
||||
self.deletePyc()
|
||||
|
||||
# Notify before returning and restarting
|
||||
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
||||
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed updating via GIT: %s' % traceback.format_exc())
|
||||
|
||||
@@ -76,32 +76,10 @@ var UpdaterBase = new Class({
|
||||
|
||||
Api.request('updater.update', {
|
||||
'onComplete': function(json){
|
||||
|
||||
if(json.success){
|
||||
App.restart();
|
||||
|
||||
$(document.body).set('spin', {
|
||||
'message': 'Updating'
|
||||
});
|
||||
$(document.body).spin();
|
||||
|
||||
var checks = 0;
|
||||
var interval = 0;
|
||||
interval = setInterval(function(){
|
||||
Api.request('', {
|
||||
'onSuccess': function(){
|
||||
if(checks > 2){
|
||||
clearInterval(interval);
|
||||
$(document.body).unspin();
|
||||
self.info();
|
||||
}
|
||||
}
|
||||
});
|
||||
checks++;
|
||||
}, 500)
|
||||
|
||||
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||
App.checkAvailable.delay(500, App);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ class Downloader(Plugin):
|
||||
def download(self, data = {}):
|
||||
pass
|
||||
|
||||
def createFileName(self, data, filename, movie):
|
||||
name = os.path.join('%s%s' % (toSafeString(data.get('name')), self.cpTag(movie)))
|
||||
if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filename:
|
||||
def createNzbName(self, data, movie):
|
||||
return '%s%s' % (toSafeString(data.get('name')), self.cpTag(movie))
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('type'))
|
||||
|
||||
@@ -30,10 +33,10 @@ class Downloader(Plugin):
|
||||
|
||||
return ''
|
||||
|
||||
def isCorrectType(self, type):
|
||||
is_correct = type in self.type
|
||||
def isCorrectType(self, item_type):
|
||||
is_correct = item_type in self.type
|
||||
|
||||
if not is_correct:
|
||||
log.debug("Downloader doesn't support this type")
|
||||
|
||||
return bool
|
||||
return is_correct
|
||||
|
||||
@@ -5,6 +5,7 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'blackhole',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
|
||||
@@ -22,7 +22,7 @@ class NZBGet(Downloader):
|
||||
log.info('Sending "%s" to NZBGet.' % data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
nzb_name = data.get('name') + '.nzb'
|
||||
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
|
||||
@@ -39,7 +39,7 @@ class Sabnzbd(Downloader):
|
||||
'apikey': self.conf('api_key'),
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': '%s%s' % (data.get('name'), self.cpTag(movie)),
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
}
|
||||
|
||||
if isfunction(data.get('download')):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from axl.axel import Event
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.helpers.variable import mergeDicts, natcmp
|
||||
from couchpotato.core.logger import CPLog
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -36,7 +35,7 @@ def addEvent(name, handler, priority = 100):
|
||||
|
||||
return h
|
||||
|
||||
e.handle(handler, priority = priority)
|
||||
e.handle(createHandle, priority = priority)
|
||||
|
||||
def removeEvent(name, handler):
|
||||
e = events[name]
|
||||
@@ -84,7 +83,8 @@ def fireEvent(name, *args, **kwargs):
|
||||
results = None
|
||||
|
||||
# Loop over results, stop when first not None result is found.
|
||||
for r in result:
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
r = result[r_key]
|
||||
if r[0] is True and r[1] is not None:
|
||||
results = r[1]
|
||||
break
|
||||
@@ -95,7 +95,8 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
else:
|
||||
results = []
|
||||
for r in result:
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
r = result[r_key]
|
||||
if r[0] == True and r[1]:
|
||||
results.append(r[1])
|
||||
elif r[1]:
|
||||
|
||||
@@ -70,7 +70,7 @@ def jsonify(mimetype, *args, **kwargs):
|
||||
|
||||
def jsonified(*args, **kwargs):
|
||||
from couchpotato.environment import Env
|
||||
callback = getParam('json_callback', None)
|
||||
callback = getParam('callback_func', None)
|
||||
if callback:
|
||||
return padded_jsonify(callback, *args, **kwargs)
|
||||
else:
|
||||
|
||||
@@ -51,8 +51,13 @@ class Loader(object):
|
||||
did_save += self.loadSettings(m, module_name, save = False)
|
||||
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
except ImportError:
|
||||
log.debug('Import error, remove the empty folder: %s' % plugin.get('module'))
|
||||
except ImportError as e:
|
||||
# todo:: subclass ImportError for missing requirements.
|
||||
if (e.message.lower().startswith("missing")):
|
||||
log.error(e.message)
|
||||
pass
|
||||
# todo:: this needs to be more descriptive.
|
||||
log.error('Import error, remove the empty folder: %s' % plugin.get('module'))
|
||||
except:
|
||||
log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc()))
|
||||
|
||||
|
||||
@@ -23,14 +23,15 @@ class Notification(Plugin):
|
||||
# Attach listeners
|
||||
for listener in self.listen_to:
|
||||
if not listener in self.dont_listen_to:
|
||||
addEvent(listener, self.createNotifyHandler(listener))
|
||||
|
||||
# Add on snatch default
|
||||
def notify(message, data):
|
||||
if not self.conf('on_snatch', default = 1) and listener == 'movie.snatched':
|
||||
return
|
||||
return self.notify(message = message, data = data)
|
||||
def createNotifyHandler(self, listener):
|
||||
def notify(message, data):
|
||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||
return
|
||||
return self.notify(message = message, data = data)
|
||||
|
||||
addEvent(listener, notify)
|
||||
return notify
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -11,28 +16,104 @@ log = CPLog(__name__)
|
||||
class CoreNotifier(Notification):
|
||||
|
||||
messages = []
|
||||
listen_to = [
|
||||
'movie.downloaded', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.frontend', self.frontend)
|
||||
|
||||
addApiView('core_notifier.listener', self.listener)
|
||||
addApiView('notification.markread', self.markAsRead, docs = {
|
||||
'desc': 'Mark notifications as read',
|
||||
'params': {
|
||||
'id': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'},
|
||||
},
|
||||
})
|
||||
|
||||
addApiView('notification.list', self.listView, docs = {
|
||||
'desc': 'Get list of notifications',
|
||||
'params': {
|
||||
'limit_offset': {'desc': 'Limit and offset the notification list. Examples: "50" or "50,30"'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any notification returned or not,
|
||||
'notifications': array, notifications found,
|
||||
}"""}
|
||||
})
|
||||
|
||||
addApiView('notification.listener', self.listener)
|
||||
|
||||
self.registerEvents()
|
||||
|
||||
|
||||
def registerEvents(self):
|
||||
|
||||
# Library update, frontend refresh
|
||||
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
self.add(data = {
|
||||
'message': message,
|
||||
'raw': data,
|
||||
def markAsRead(self):
|
||||
ids = getParam('ids').split(',')
|
||||
|
||||
db = get_session()
|
||||
|
||||
q = db.query(Notif) \
|
||||
.filter(or_(*[Notif.id == tryInt(s) for s in ids]))
|
||||
q.update({Notif.read: True})
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
|
||||
def listView(self):
|
||||
|
||||
db = get_session()
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
|
||||
q = db.query(Notif)
|
||||
|
||||
if limit_offset:
|
||||
splt = limit_offset.split(',')
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
results = q.all()
|
||||
notifications = []
|
||||
for n in results:
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
'notifications': notifications
|
||||
})
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
db = get_session()
|
||||
|
||||
n = Notif(
|
||||
message = toUnicode(message),
|
||||
data = data
|
||||
)
|
||||
db.add(n)
|
||||
db.commit()
|
||||
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
ndict['time'] = time.time()
|
||||
self.messages.append(ndict)
|
||||
|
||||
db.remove()
|
||||
|
||||
def frontend(self, type = 'notification', data = {}):
|
||||
self.messages.append({
|
||||
'time': time.time(),
|
||||
@@ -48,6 +129,18 @@ class CoreNotifier(Notification):
|
||||
if message['time'] > (time.time() - 15):
|
||||
messages.append(message)
|
||||
|
||||
# Get unread
|
||||
if getParam('init'):
|
||||
db = get_session()
|
||||
|
||||
notifications = db.query(Notif) \
|
||||
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
|
||||
.all()
|
||||
for n in notifications:
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
self.messages = []
|
||||
return jsonified({
|
||||
'success': True,
|
||||
|
||||
@@ -10,19 +10,86 @@ var NotificationBase = new Class({
|
||||
// Listener
|
||||
App.addEvent('load', self.startInterval.bind(self));
|
||||
App.addEvent('unload', self.stopTimer.bind(self));
|
||||
self.addEvent('notification', self.notify.bind(self))
|
||||
App.addEvent('notification', self.notify.bind(self));
|
||||
|
||||
// Add test buttons to settings page
|
||||
App.addEvent('load', self.addTestButtons.bind(self));
|
||||
|
||||
// Notification bar
|
||||
self.notifications = []
|
||||
App.addEvent('load', function(){
|
||||
|
||||
App.block.notification = new Block.Menu(self, {
|
||||
'class': 'notification_menu',
|
||||
'onOpen': self.markAsRead.bind(self)
|
||||
})
|
||||
$(App.block.notification).inject(App.getBlock('search'), 'after');
|
||||
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
|
||||
|
||||
/* App.getBlock('notification').addLink(new Element('a.more', {
|
||||
'href': App.createUrl('notifications'),
|
||||
'text': 'Show older notifications'
|
||||
})); */
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
notify: function(result){
|
||||
var self = this;
|
||||
|
||||
var added = new Date();
|
||||
added.setTime(result.added*1000)
|
||||
|
||||
result.el = App.getBlock('notification').addLink(
|
||||
new Element('span.'+(result.read ? 'read' : '' )).adopt(
|
||||
new Element('span.message', {'text': result.message}),
|
||||
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
|
||||
)
|
||||
, 'top');
|
||||
self.notifications.include(result);
|
||||
|
||||
if(!result.read)
|
||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||
|
||||
},
|
||||
|
||||
setBadge: function(value){
|
||||
var self = this;
|
||||
self.badge.set('text', value)
|
||||
self.badge[value ? 'show' : 'hide']()
|
||||
},
|
||||
|
||||
markAsRead: function(){
|
||||
var self = this;
|
||||
|
||||
var rn = self.notifications.filter(function(n){
|
||||
return !n.read
|
||||
})
|
||||
|
||||
var ids = []
|
||||
rn.each(function(n){
|
||||
ids.include(n.id)
|
||||
})
|
||||
|
||||
if(ids.length > 0)
|
||||
Api.request('notification.markread', {
|
||||
'data': {
|
||||
'ids': ids.join(',')
|
||||
},
|
||||
'onSuccess': function(){
|
||||
self.setBadge('')
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
startInterval: function(){
|
||||
var self = this;
|
||||
|
||||
self.request = Api.request('core_notifier.listener', {
|
||||
self.request = Api.request('notification.listener', {
|
||||
'initialDelay': 100,
|
||||
'delay': 3000,
|
||||
'data': {'init':true},
|
||||
'onSuccess': self.processData.bind(self)
|
||||
})
|
||||
|
||||
@@ -40,16 +107,12 @@ var NotificationBase = new Class({
|
||||
this.request.stopTimer()
|
||||
},
|
||||
|
||||
notify: function(data){
|
||||
var self = this;
|
||||
|
||||
},
|
||||
|
||||
processData: function(json){
|
||||
var self = this;
|
||||
|
||||
self.request.options.data = {}
|
||||
Array.each(json.result, function(result){
|
||||
App.fireEvent(result.type, result.data)
|
||||
App.fireEvent(result.type, result)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Growl(Notification):
|
||||
except:
|
||||
log.error('Failed register of growl: %s' % traceback.format_exc())
|
||||
|
||||
def notify(self, type = '', message = '', data = {}):
|
||||
def notify(self, message = '', data = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
self.register()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from .main import NotifyMyWP
|
||||
|
||||
def start():
|
||||
return NotifyMyWP()
|
||||
|
||||
config = [{
|
||||
'name': 'notifymywp',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'notifymywp',
|
||||
'label': 'Notify My Windows Phone',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'description': 'Multiple keys seperated by a comma. Maximum of 5.'
|
||||
},
|
||||
{
|
||||
'name': 'dev_key',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'default': 0,
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)],
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,23 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from pynmwp import PyNMWP
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NotifyMyWP(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
keys = self.conf('api_key').split(',')
|
||||
p = PyNMWP(keys, self.conf('dev_key'))
|
||||
|
||||
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
|
||||
|
||||
for key in keys:
|
||||
if not response[key]['Code'] == u'200':
|
||||
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s' % (key, response[key]['message']))
|
||||
return False
|
||||
|
||||
return response
|
||||
@@ -30,7 +30,7 @@ class XBMC(Notification):
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(url, headers = headers)
|
||||
self.urlopen(url, headers = headers, show_error = False)
|
||||
except:
|
||||
log.error("Couldn't sent command to XBMC")
|
||||
return False
|
||||
|
||||
@@ -5,6 +5,7 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'automation',
|
||||
'order': 30,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
|
||||
@@ -98,7 +98,7 @@ class Plugin(object):
|
||||
if not headers.get('Referer'):
|
||||
headers['Referer'] = urlparse(url).hostname
|
||||
if not headers.get('User-Agent'):
|
||||
headers['User-Agent'] = ''
|
||||
headers['User-Agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2'
|
||||
|
||||
host = urlparse(url).hostname
|
||||
self.wait(host)
|
||||
|
||||
@@ -6,12 +6,31 @@ import os
|
||||
import string
|
||||
|
||||
if os.name == 'nt':
|
||||
import win32file
|
||||
import imp
|
||||
try:
|
||||
imp.find_module('win32file')
|
||||
except:
|
||||
# todo:: subclass ImportError for missing dependencies, vs. broken plugins?
|
||||
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
|
||||
else:
|
||||
import win32file
|
||||
|
||||
class FileBrowser(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('directory.list', self.view)
|
||||
addApiView('directory.list', self.view, docs = {
|
||||
'desc': 'Return the directory list of a given directory',
|
||||
'params': {
|
||||
'path': {'desc': 'The directory to scan'},
|
||||
'show_hidden': {'desc': 'Also show hidden files'}
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'is_root': bool, //is top most folder
|
||||
'empty': bool, //directory is empty
|
||||
'dirs': array, //directory names
|
||||
}"""}
|
||||
})
|
||||
|
||||
def getDirectories(self, path = '/', show_hidden = True):
|
||||
|
||||
@@ -27,7 +46,7 @@ class FileBrowser(Plugin):
|
||||
if os.path.isdir(p) and ((self.is_hidden(p) and bool(int(show_hidden))) or not self.is_hidden(p)):
|
||||
dirs.append(p + os.path.sep)
|
||||
|
||||
return dirs
|
||||
return sorted(dirs)
|
||||
|
||||
def getFiles(self):
|
||||
pass
|
||||
|
||||
@@ -19,7 +19,13 @@ class FileManager(Plugin):
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True)
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = {
|
||||
'desc': 'Return a file from the cp_data/cache directory',
|
||||
'params': {
|
||||
'filename': {'desc': 'path/filename of the wanted file'}
|
||||
},
|
||||
'return': {'type': 'file'}
|
||||
})
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
var self = this;
|
||||
|
||||
|
||||
if(!file){
|
||||
self.el = new Element('div');
|
||||
return
|
||||
@@ -17,7 +17,7 @@ var File = new Class({
|
||||
|
||||
createImage: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
var file_name = self.data.path.replace(/^.*[\\\/]/, '');
|
||||
|
||||
self.el = new Element('div', {
|
||||
|
||||
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
from string import ascii_letters
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -75,7 +74,7 @@ class LibraryPlugin(Plugin):
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.info = toUnicode(json.dumps(info))
|
||||
library.info = info
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
@@ -85,6 +84,8 @@ class LibraryPlugin(Plugin):
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s' % titles)
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
t = LibraryTitle(
|
||||
title = toUnicode(title),
|
||||
simple_title = self.simplifyTitle(title),
|
||||
|
||||
@@ -12,9 +12,27 @@ log = CPLog(__name__)
|
||||
class Logging(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('logging.get', self.get)
|
||||
addApiView('logging.clear', self.clear)
|
||||
addApiView('logging.log', self.log)
|
||||
addApiView('logging.get', self.get, docs = {
|
||||
'desc': 'Get the full log file by number',
|
||||
'params': {
|
||||
'nr': {'desc': 'Number of the log to get.'}
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'log': string, //Log file
|
||||
'total': int, //Total log files available
|
||||
}"""}
|
||||
})
|
||||
addApiView('logging.clear', self.clear, docs = {
|
||||
'desc': 'Remove all the log files'
|
||||
})
|
||||
addApiView('logging.log', self.log, docs = {
|
||||
'desc': 'Get the full log file by number',
|
||||
'params': {
|
||||
'type': {'desc': 'Type of logging, default "error"'},
|
||||
'**kwargs': {'type':'object', 'desc': 'All other params will be printed in the log string.'},
|
||||
}
|
||||
})
|
||||
|
||||
def get(self):
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
float: left;
|
||||
width: 86%;
|
||||
line-height: 150%;
|
||||
padding: 3px 1%;
|
||||
padding: 3px 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
font-size: 11px;
|
||||
font-family: Lucida Console, Monaco, Nimbus Mono L;
|
||||
@@ -56,10 +56,10 @@
|
||||
|
||||
.page.log .container .time {
|
||||
clear: both;
|
||||
width: 11%;
|
||||
width: 14%;
|
||||
color: lightgrey;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
|
||||
.page.log .container .time:last-child { display: none; }
|
||||
|
||||
|
||||
@@ -4,6 +4,20 @@ Page.Log = new Class({
|
||||
|
||||
name: 'log',
|
||||
title: 'Show recent logs.',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options)
|
||||
|
||||
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}))
|
||||
|
||||
},
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -7,10 +7,9 @@ config = [{
|
||||
'name': 'manage',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'renamer',
|
||||
'tab': 'manage',
|
||||
'label': 'movie library manager',
|
||||
'description': 'Add your existing movie folders.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -21,7 +21,12 @@ class Manage(Plugin):
|
||||
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
|
||||
|
||||
addEvent('manage.update', self.updateLibrary)
|
||||
addApiView('manage.update', self.updateLibraryView)
|
||||
addApiView('manage.update', self.updateLibraryView, docs = {
|
||||
'desc': 'Update the library by scanning for new movies',
|
||||
'params': {
|
||||
'full': {'desc': 'Do a full update or just recently changed/added movies.'},
|
||||
}
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.updateLibrary)
|
||||
|
||||
@@ -26,14 +26,60 @@ class MoviePlugin(Plugin):
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
addApiView('movie.search', self.search)
|
||||
addApiView('movie.list', self.listView)
|
||||
addApiView('movie.refresh', self.refresh)
|
||||
addApiView('movie.search', self.search, docs = {
|
||||
'desc': 'Search the movie providers for a movie',
|
||||
'params': {
|
||||
'q': {'desc': 'The (partial) movie name you want to search for'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.list', self.listView, docs = {
|
||||
'desc': 'List movies in wanted list',
|
||||
'params': {
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.refresh', self.refresh, docs = {
|
||||
'desc': 'Refresh a movie by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the movie that needs to be refreshed'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.available_chars', self.charView)
|
||||
|
||||
addApiView('movie.add', self.addView)
|
||||
addApiView('movie.edit', self.edit)
|
||||
addApiView('movie.delete', self.delete)
|
||||
addApiView('movie.add', self.addView, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
|
||||
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.edit', self.edit, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
|
||||
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.delete', self.delete, docs = {
|
||||
'desc': 'Delete a movie from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('movie.add', self.add)
|
||||
addEvent('movie.get', self.get)
|
||||
@@ -55,19 +101,11 @@ class MoviePlugin(Plugin):
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
|
||||
|
||||
q = db.query(Movie) \
|
||||
.join(Movie.library, Library.titles) \
|
||||
.options(joinedload_all('releases.status')) \
|
||||
.options(joinedload_all('releases.quality')) \
|
||||
.options(joinedload_all('releases.files')) \
|
||||
.options(joinedload_all('releases.info')) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
@@ -88,17 +126,32 @@ class MoviePlugin(Plugin):
|
||||
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
q = q.subquery()
|
||||
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files')) \
|
||||
|
||||
|
||||
if limit_offset:
|
||||
splt = limit_offset.split(',')
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
q2 = q2.limit(limit).offset(offset)
|
||||
|
||||
results = q.all()
|
||||
|
||||
results = q2.all()
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = movie.to_dict(self.default_dict)
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movies.append(temp)
|
||||
|
||||
return movies
|
||||
@@ -117,7 +170,8 @@ class MoviePlugin(Plugin):
|
||||
.join(Movie.library, Library.titles) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
results = q.all()
|
||||
|
||||
@@ -230,6 +284,13 @@ class MoviePlugin(Plugin):
|
||||
|
||||
db.commit()
|
||||
|
||||
# Remove releases
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
|
||||
if force_readd or do_search:
|
||||
@@ -255,19 +316,31 @@ class MoviePlugin(Plugin):
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Movie).filter_by(id = params.get('id')).first()
|
||||
m.profile_id = params.get('profile_id')
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
# Default title
|
||||
for title in m.library.titles:
|
||||
title.default = params.get('default_title').lower() == title.title.lower()
|
||||
ids = params.get('id').split(',')
|
||||
for movie_id in ids:
|
||||
|
||||
db.commit()
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
m.profile_id = params.get('profile_id')
|
||||
|
||||
fireEvent('movie.restatus', m.id)
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict)
|
||||
# Default title
|
||||
if params.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = params.get('default_title').lower() == title.title.lower()
|
||||
|
||||
db.commit()
|
||||
|
||||
fireEvent('movie.restatus', m.id)
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
@@ -280,9 +353,11 @@ class MoviePlugin(Plugin):
|
||||
|
||||
status = fireEvent('status.add', 'deleted', single = True)
|
||||
|
||||
movie = db.query(Movie).filter_by(id = params.get('id')).first()
|
||||
movie.status_id = status.get('id')
|
||||
db.commit()
|
||||
ids = params.get('id').split(',')
|
||||
for movie_id in ids:
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
movie.status_id = status.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
|
||||
@@ -3,8 +3,9 @@ var MovieList = new Class({
|
||||
Implements: [Options],
|
||||
|
||||
options: {
|
||||
navigation: false,
|
||||
limit: 50
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
menu: []
|
||||
},
|
||||
|
||||
movies: [],
|
||||
@@ -69,18 +70,23 @@ var MovieList = new Class({
|
||||
self.scrollspy.stop();
|
||||
}
|
||||
|
||||
Object.each(movies, function(info){
|
||||
Object.each(movies, function(movie){
|
||||
|
||||
// Attach proper actions
|
||||
var a = self.options.actions
|
||||
var actions = a[info.status.identifier.capitalize()] || a.Wanted || {};
|
||||
var a = self.options.actions,
|
||||
status = Status.get(movie.status_id);
|
||||
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
|
||||
|
||||
var m = new Movie(self, {
|
||||
'actions': actions
|
||||
}, info);
|
||||
'actions': actions,
|
||||
'view': self.current_view,
|
||||
'onSelect': self.calculateSelected.bind(self)
|
||||
}, movie);
|
||||
$(m).inject(self.movie_list);
|
||||
m.fireEvent('injected');
|
||||
|
||||
self.movies.include(m)
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
@@ -89,8 +95,12 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.current_view = self.getSavedView();
|
||||
self.el.addClass(self.current_view+'_list')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.alpha = new Element('ul.inlay', {
|
||||
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
self.movie_list.empty()
|
||||
@@ -99,24 +109,74 @@ var MovieList = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.search_input = new Element('input.inlay', {
|
||||
self.navigation_search_input = new Element('input.inlay', {
|
||||
'placeholder': 'Search',
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})/*,
|
||||
self.view = new Element('ul.inlay').adopt(
|
||||
new Element('li.list'),
|
||||
new Element('li.thumbnails'),
|
||||
new Element('li.text')
|
||||
)*/
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self),
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
// Mass edit
|
||||
self.mass_edit_select_class = new Form.Check(self.mass_edit_select);
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'thumbs', 'list'].each(function(view){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(this.get('data-view'));
|
||||
this.addClass(a);
|
||||
}
|
||||
}
|
||||
}).adopt(new Element('span'))
|
||||
)
|
||||
});
|
||||
|
||||
// All
|
||||
self.letters['all'] = new Element('li.letter_all.available.active', {
|
||||
'text': 'ALL',
|
||||
}).inject(self.alpha);
|
||||
}).inject(self.navigation_alpha);
|
||||
|
||||
// Chars
|
||||
chars.split('').each(function(c){
|
||||
@@ -124,7 +184,7 @@ var MovieList = new Class({
|
||||
'text': c,
|
||||
'class': 'letter_'+c,
|
||||
'data-letter': c
|
||||
}).inject(self.alpha);
|
||||
}).inject(self.navigation_alpha);
|
||||
});
|
||||
|
||||
// Get available chars and highlight
|
||||
@@ -141,12 +201,126 @@ var MovieList = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
// Add menu or hide
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
})
|
||||
else
|
||||
self.navigation_menu.hide()
|
||||
|
||||
self.nav_scrollspy = new ScrollSpy({
|
||||
min: 10,
|
||||
onEnter: function(){
|
||||
self.navigation.addClass('float')
|
||||
},
|
||||
onLeave: function(){
|
||||
self.navigation.removeClass('float')
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
calculateSelected: function(){
|
||||
var self = this;
|
||||
|
||||
var selected = 0,
|
||||
movies = self.movies.length;
|
||||
self.movies.each(function(movie){
|
||||
selected += movie.isSelected() ? 1 : 0
|
||||
})
|
||||
|
||||
var indeterminate = selected > 0 && selected < movies,
|
||||
checked = selected == movies && selected > 0;
|
||||
|
||||
self.mass_edit_select.set('indeterminate', indeterminate)
|
||||
|
||||
self.mass_edit_select_class[checked ? 'check' : 'uncheck']()
|
||||
self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate')
|
||||
|
||||
self.mass_edit_selected.set('text', selected);
|
||||
},
|
||||
|
||||
deleteSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies()
|
||||
|
||||
var qObj = new Question('Are you sure you want to delete the selected movies?', 'Items using this profile, will be set to the default quality.', [{
|
||||
'text': 'Yes, delete them',
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': ids.join(',')
|
||||
},
|
||||
'onSuccess': function(){
|
||||
qObj.close();
|
||||
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy()
|
||||
self.movies.erase(movie)
|
||||
}
|
||||
});
|
||||
|
||||
self.calculateSelected()
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'text': 'Cancel',
|
||||
'cancel': true
|
||||
}]);
|
||||
|
||||
},
|
||||
|
||||
changeQualitySelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies()
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'profile_id': self.mass_edit_quality.get('value')
|
||||
},
|
||||
'onSuccess': self.search.bind(self)
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedMovies: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = []
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected())
|
||||
ids.include(movie.get('id'))
|
||||
});
|
||||
|
||||
return ids
|
||||
},
|
||||
|
||||
massEditToggleAll: function(){
|
||||
var self = this;
|
||||
|
||||
var select = self.mass_edit_select.get('checked');
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.select(select)
|
||||
});
|
||||
|
||||
self.calculateSelected()
|
||||
},
|
||||
|
||||
reset: function(){
|
||||
var self = this;
|
||||
|
||||
self.navigation.getElements('.active').removeClass('active')
|
||||
self.movies = []
|
||||
self.calculateSelected()
|
||||
self.navigation_alpha.getElements('.active').removeClass('active')
|
||||
self.offset = 0;
|
||||
self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
@@ -162,12 +336,32 @@ var MovieList = new Class({
|
||||
|
||||
},
|
||||
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.changeView(new_view)
|
||||
});
|
||||
|
||||
self.el
|
||||
.removeClass(self.current_view+'_list')
|
||||
.addClass(new_view+'_list')
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view') || 'thumbs';
|
||||
},
|
||||
|
||||
search: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.search_timer) clearTimeout(self.search_timer);
|
||||
self.search_timer = (function(){
|
||||
var search_value = self.search_input.get('value');
|
||||
var search_value = self.navigation_search_input.get('value');
|
||||
if (search_value == self.last_search_value) return
|
||||
|
||||
self.reset()
|
||||
@@ -211,8 +405,8 @@ var MovieList = new Class({
|
||||
|
||||
loadMore: function(){
|
||||
var self = this;
|
||||
|
||||
self.getMovies()
|
||||
if(self.offset >= self.options.limit)
|
||||
self.getMovies()
|
||||
},
|
||||
|
||||
store: function(movies){
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
/* @override
|
||||
http://localhost:5000/static/movie_plugin/movie.css
|
||||
http://192.168.1.20:5000/static/movie_plugin/movie.css
|
||||
http://127.0.0.1:5000/static/movie_plugin/movie.css
|
||||
*/
|
||||
|
||||
.movies {
|
||||
padding: 20px 0;
|
||||
padding: 60px 0 20px;
|
||||
}
|
||||
|
||||
.movies.mass_edit_list {
|
||||
padding-top: 90px;
|
||||
}
|
||||
|
||||
.movies .movie {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.movies .movie:hover {
|
||||
border-color: #ddd #fff #fff #ddd;
|
||||
.movies .movie.list_view, .movies .movie.mass_edit_view {
|
||||
margin: 1px 0;
|
||||
border-radius: 0;
|
||||
background: no-repeat;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.movies .data {
|
||||
padding: 20px;
|
||||
height: 140px;
|
||||
width: 800px;
|
||||
height: 180px;
|
||||
width: 840px;
|
||||
position: relative;
|
||||
float: right;
|
||||
|
||||
border-radius: 0;
|
||||
-moz-border-radius: 0;
|
||||
-webkit-border-radius: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.movies .list_view .data, .movies .mass_edit_view .data {
|
||||
height: 30px;
|
||||
padding: 3px 0 3px 10px;
|
||||
width: 938px;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies .movie .check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.mass_edit_list .movie .check {
|
||||
float: left;
|
||||
display: block;
|
||||
margin: 7px 0 0 5px;
|
||||
}
|
||||
|
||||
.movies .poster {
|
||||
float: left;
|
||||
width: 120px;
|
||||
@@ -37,21 +61,37 @@
|
||||
overflow: hidden;
|
||||
height: 180px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
transition: all 0.2s linear;
|
||||
|
||||
}
|
||||
.movies .list_view .poster, .movies .mass_edit_view .poster {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
}
|
||||
.movies.mass_edit_list .poster {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .poster img, .options .poster img {
|
||||
width: 101%;
|
||||
height: 101%;
|
||||
}
|
||||
|
||||
|
||||
.movies .info .title {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
float: left;
|
||||
width: 80%;
|
||||
width: 90%;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
|
||||
.movies .list_view .info .title, .movies .mass_edit_view .info .title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
text-overflow: ellipsis;
|
||||
width: 64%;
|
||||
}
|
||||
|
||||
.movies .info .year {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
@@ -59,8 +99,13 @@
|
||||
color: #bbb;
|
||||
width: 10%;
|
||||
text-align: right;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
|
||||
.movies .list_view .info .year, .movies .mass_edit_view .info .year {
|
||||
font-size: 16px;
|
||||
width: 6%;
|
||||
}
|
||||
|
||||
.movies .info .rating {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
@@ -69,7 +114,7 @@
|
||||
width: 5%;
|
||||
padding: 0 0 0 3%;
|
||||
}
|
||||
|
||||
|
||||
.movies .info .description {
|
||||
clear: both;
|
||||
height: 80px;
|
||||
@@ -78,35 +123,85 @@
|
||||
.movies .data:hover .description {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.movies .data .quality span {
|
||||
padding: 0 5px;
|
||||
font-weight: bold;
|
||||
.movies .list_view .info .description, .movies .mass_edit_view .info .description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .data .quality {
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
vertical-align: mid;
|
||||
}
|
||||
.movies .data .quality span:first-child {padding-left: 0;}
|
||||
|
||||
.movies .data .quality .available { color: orange; }
|
||||
.movies .data .quality .snatched { color: lightgreen; }
|
||||
|
||||
|
||||
.movies .data .quality span {
|
||||
padding: 2px 3px;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 12px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
|
||||
text-align: right;
|
||||
float: right;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.movies .data .quality .available, .movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .snatched { background-color: #369545; }
|
||||
.movies .data .quality .done {
|
||||
background-color: #369545;
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .data .quality .finish {
|
||||
background-image: url('../images/sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 2px;
|
||||
padding-left: 14px;
|
||||
background-size: 14px
|
||||
}
|
||||
|
||||
.movies .data .actions {
|
||||
line-height: 0;
|
||||
clear: both;
|
||||
float: right;
|
||||
margin-top: -25px;
|
||||
}
|
||||
.movies .data:hover .action { opacity: 0.6; }
|
||||
.movies .data:hover .action:hover { opacity: 1; }
|
||||
|
||||
.movies .data:hover .action { opacity: 0.6; }
|
||||
.movies .data:hover .action:hover { opacity: 1; }
|
||||
.movies.mass_edit_list .data .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .data .action {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
|
||||
margin: -34px 2px 0 0;
|
||||
background: #4e5969;
|
||||
}
|
||||
|
||||
.movies .delete_container {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
@@ -128,22 +223,23 @@
|
||||
color: #fff;
|
||||
background-color: #d32917;
|
||||
}
|
||||
|
||||
|
||||
.movies .options {
|
||||
position: absolute;
|
||||
margin-left: 120px;
|
||||
width: 840px;
|
||||
}
|
||||
|
||||
|
||||
.movies .options .form {
|
||||
margin: 70px 20px 0;
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
.movies .options .form select {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
|
||||
.movies .options .table {
|
||||
height: 180px;
|
||||
overflow: auto;
|
||||
@@ -151,18 +247,27 @@
|
||||
.movies .options .table .item {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .options .table .item.ignored span {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255,255,255,0.4);
|
||||
text-shadow: none;
|
||||
}
|
||||
.movies .options .table .item.ignored .delete {
|
||||
background-image: url('../images/icon.undo.png');
|
||||
}
|
||||
|
||||
.movies .options .table .item:last-child { border: 0; }
|
||||
.movies .options .table .item:nth-child(even) {
|
||||
.movies .options .table .item:nth-child(even) {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.movies .options .table .item:not(.head):hover {
|
||||
.movies .options .table .item:not(.head):hover {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
|
||||
.movies .options .table .item > * {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
width: 50px;
|
||||
width: 60px;
|
||||
min-height: 24px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -174,21 +279,21 @@
|
||||
border: 0;
|
||||
}
|
||||
.movies .options .table .provider {
|
||||
width: 120px;
|
||||
width: 130px;
|
||||
}
|
||||
.movies .options .table .name {
|
||||
width: 360px;
|
||||
width: 370px;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.movies .options .table.files .name { width: 598px; }
|
||||
.movies .options .table .type { width: 120px; }
|
||||
.movies .options .table .is_available { width: 80px; }
|
||||
|
||||
.movies .options .table.files .name { width: 605px; }
|
||||
.movies .options .table .type { width: 130px; }
|
||||
.movies .options .table .is_available { width: 90px; }
|
||||
|
||||
.movies .options .table a {
|
||||
width: 16px !important;
|
||||
height: 16px;
|
||||
width: 30px !important;
|
||||
height: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.movies .options .table a:hover {
|
||||
@@ -202,7 +307,7 @@
|
||||
padding-bottom: 4px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .load_more {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
@@ -214,10 +319,23 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav {
|
||||
overflow: hidden;
|
||||
transition: box-shadow .4s linear;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
padding: 100px 60px 7px;
|
||||
width: 1080px;
|
||||
margin: 0 -60px;
|
||||
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.movies .alph_nav ul {
|
||||
|
||||
.movies .alph_nav.float {
|
||||
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
|
||||
border-radius: 0;
|
||||
background: #4e5969;
|
||||
}
|
||||
|
||||
.movies .alph_nav ul.numbers, .movies .alph_nav ul.actions {
|
||||
list-style: none;
|
||||
padding: 0 0 1px;
|
||||
margin: 0;
|
||||
@@ -225,30 +343,129 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.movies .alph_nav li {
|
||||
.movies .alph_nav .numbers li, .movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 24px;
|
||||
width: 22px;
|
||||
height: 24px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
color: rgba(255,255,255,0.2);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.1s ease-in-out;
|
||||
text-shadow: none;
|
||||
}
|
||||
.movies .alph_nav li:first-child {
|
||||
width: 34px;
|
||||
}
|
||||
.movies .alph_nav li.active, .movies .alph_nav li:hover {
|
||||
font-weight: bolder;
|
||||
color: #fff;
|
||||
.movies .alph_nav .numbers li:first-child {
|
||||
width: 43px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
.movies .alph_nav li.available {
|
||||
color: #fff;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-weight: bolder;
|
||||
|
||||
}
|
||||
|
||||
.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.movies .alph_nav input {
|
||||
padding: 6px 5px;
|
||||
margin: 0;
|
||||
float: right;
|
||||
}
|
||||
margin: 0 0 0 6px;
|
||||
float: left;
|
||||
width: 155px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions {
|
||||
margin: 0 6px 0 0;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.movies .alph_nav .actions li {
|
||||
border-radius: 1px;
|
||||
width: auto;
|
||||
}
|
||||
.movies .alph_nav .actions li.active {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.movies .alph_nav .actions li span {
|
||||
display: block;
|
||||
background: url('../images/sprite.png') no-repeat;
|
||||
width: 25px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.mass_edit span {
|
||||
background-position: 3px 3px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.list span {
|
||||
background-position: 3px -95px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.thumbs span {
|
||||
background-position: 3px -74px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.movies .alph_nav .actions li:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form {
|
||||
display: block;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select {
|
||||
float: left;
|
||||
margin: 5px 0 0 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select span {
|
||||
vertical-align: middle;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select .count {
|
||||
font-weight: bold;
|
||||
margin: 0 3px 0 10px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .quality {
|
||||
float: left;
|
||||
padding: 8px 0 0;
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
.movies .alph_nav .mass_edit_form .quality select {
|
||||
width: 120px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.movies .alph_nav .mass_edit_form .button {
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .delete {
|
||||
float: left;
|
||||
padding: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .delete span {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu {
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu > a {
|
||||
background-position: center -157px;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.view = options.view || 'thumbs';
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.parent(self, options);
|
||||
@@ -17,6 +18,13 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('div.movie.inlay').adopt(
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.data_container = new Element('div.data.inlay.light', {
|
||||
'tween': {
|
||||
@@ -38,12 +46,23 @@ var Movie = new Class({
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
self.quality = new Element('div.quality')
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
)
|
||||
);
|
||||
|
||||
self.changeView(self.view);
|
||||
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||
|
||||
// Add profile
|
||||
if(self.profile.data)
|
||||
self.profile.getTypes().each(function(type){
|
||||
@@ -57,11 +76,13 @@ var Movie = new Class({
|
||||
// Add done releases
|
||||
Array.each(self.data.releases, function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_'+ release.quality.identifier);
|
||||
if(!q)
|
||||
var q = self.addQuality(release.quality_id)
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
q.addClass(release.status.identifier);
|
||||
if(!q && status.identifier == 'snatched')
|
||||
var q = self.addQuality(release.quality_id)
|
||||
if (q)
|
||||
q.addClass(status.identifier);
|
||||
|
||||
});
|
||||
|
||||
@@ -82,7 +103,7 @@ var Movie = new Class({
|
||||
var q = Quality.getQuality(quality_id);
|
||||
return new Element('span', {
|
||||
'text': q.label,
|
||||
'class': 'q_'+q.identifier
|
||||
'class': 'q_'+q.identifier + ' q_id' + q.id
|
||||
}).inject(self.quality);
|
||||
|
||||
},
|
||||
@@ -97,18 +118,30 @@ var Movie = new Class({
|
||||
}).pop()
|
||||
|
||||
if(title)
|
||||
return title.title
|
||||
return self.getUnprefixedTitle(title.title)
|
||||
else if(titles.length > 0)
|
||||
return titles[0].title
|
||||
return self.getUnprefixedTitle(titles[0].title)
|
||||
|
||||
return 'Unknown movie'
|
||||
},
|
||||
|
||||
getUnprefixedTitle: function(t){
|
||||
if(t.substr(0, 4).toLowerCase() == 'the ')
|
||||
t = t.substr(4) + ', The';
|
||||
return t;
|
||||
},
|
||||
|
||||
slide: function(direction, el){
|
||||
var self = this;
|
||||
|
||||
if(direction == 'in'){
|
||||
self.el.addEvent('outerClick', self.slide.bind(self, 'out'))
|
||||
self.temp_view = self.view;
|
||||
self.changeView('thumbs')
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.changeView(self.temp_view)
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
self.data_container.tween('right', 0, -840);
|
||||
}
|
||||
@@ -123,8 +156,27 @@ var Movie = new Class({
|
||||
}
|
||||
},
|
||||
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
self.el
|
||||
.removeClass(self.view+'_view')
|
||||
.addClass(new_view+'_view')
|
||||
|
||||
self.view = new_view;
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.library[attr]
|
||||
},
|
||||
|
||||
select: function(bool){
|
||||
var self = this;
|
||||
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
|
||||
},
|
||||
|
||||
isSelected: function(){
|
||||
return this.select_checkbox.get('checked');
|
||||
}
|
||||
|
||||
});
|
||||
@@ -180,7 +232,7 @@ var IMDBAction = new Class({
|
||||
|
||||
gotoIMDB: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
|
||||
window.open('http://www.imdb.com/title/'+self.id+'/');
|
||||
}
|
||||
@@ -208,7 +260,7 @@ var ReleaseAction = new Class({
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
@@ -226,11 +278,15 @@ var ReleaseAction = new Class({
|
||||
).inject(self.release_container)
|
||||
|
||||
Array.each(self.movie.data.releases, function(release){
|
||||
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id)
|
||||
|
||||
new Element('div', {
|
||||
'class': 'item ' + release.status.identifier
|
||||
'class': 'item ' + status.identifier
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
||||
new Element('span.quality', {'text': release.quality.label}),
|
||||
new Element('span.quality', {'text': quality.get('label')}),
|
||||
new Element('span.size', {'text': (self.get(release, 'size') || 'unknown')}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
@@ -238,7 +294,7 @@ var ReleaseAction = new Class({
|
||||
new Element('a.download.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
self.download(release);
|
||||
}
|
||||
}
|
||||
@@ -246,9 +302,9 @@ var ReleaseAction = new Class({
|
||||
new Element('a.delete.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
self.del(release);
|
||||
this.getParent('.item').destroy();
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -278,10 +334,10 @@ var ReleaseAction = new Class({
|
||||
});
|
||||
},
|
||||
|
||||
del: function(release){
|
||||
ignore: function(release){
|
||||
var self = this;
|
||||
|
||||
Api.request('release.delete', {
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
}
|
||||
|
||||
@@ -1,73 +1,100 @@
|
||||
/* @override
|
||||
http://localhost:5000/static/movie_plugin/search.css
|
||||
http://192.168.1.20:5000/static/movie_plugin/search.css
|
||||
http://127.0.0.1:5000/static/movie_plugin/search.css
|
||||
*/
|
||||
|
||||
.search_form {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.search_form input {
|
||||
padding-right: 25px;
|
||||
padding: 4px;
|
||||
padding: 4px 20px 4px 4px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
width: 90%;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
}
|
||||
.search_form input:focus {
|
||||
padding-right: 83px;
|
||||
}
|
||||
|
||||
.search_form .input .enter {
|
||||
background: #369545 url('../images/sprite.png') right -188px no-repeat;
|
||||
padding: 0 20px 0 4px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
margin-left: -78px;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
.search_form.focused .input .enter {
|
||||
visibility: visible;
|
||||
}
|
||||
.search_form.focused.filled .input .enter {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search_form .input a {
|
||||
width: 17px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin: 0 0 -5px -20px;
|
||||
margin: -2px 0 0 2px;
|
||||
top: 4px;
|
||||
right: 5px;
|
||||
background: url('../images/checks.png') right -36px no-repeat;
|
||||
background: url('../images/sprite.png') left -37px no-repeat;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.search_form.filled .input a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search_form .results_container {
|
||||
position: absolute;
|
||||
background: #5c697b;
|
||||
margin: 6px 0 0 -246px;
|
||||
margin: 6px 0 0 -230px;
|
||||
width: 470px;
|
||||
min-height: 140px;
|
||||
|
||||
border-radius: 3px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
|
||||
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
}
|
||||
.search_form .spinner {
|
||||
background: rgba(0,0,0,0.8) url('../images/spinner.gif') no-repeat center 70px;
|
||||
}
|
||||
|
||||
.search_form .pointer {
|
||||
border-right: 10px solid transparent;
|
||||
border-left: 10px solid transparent;
|
||||
border-bottom: 10px solid #5c697b;
|
||||
.search_form.shown.filled .results_container {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
left: 50%;
|
||||
margin: -9px 0 0 110px;
|
||||
}
|
||||
|
||||
.search_form .results_container:before {
|
||||
content: ' ';
|
||||
height: 0;
|
||||
position: relative;
|
||||
width: 0;
|
||||
border: 10px solid transparent;
|
||||
border-bottom-color: #5c697b;
|
||||
display: block;
|
||||
top: -20px;
|
||||
left: 346px;
|
||||
}
|
||||
|
||||
.search_form .results {
|
||||
max-height: 550px;
|
||||
max-height: 570px;
|
||||
overflow-x: hidden;
|
||||
padding: 10px 0;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.movie_result {
|
||||
overflow: hidden;
|
||||
min-height: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.movie_result .options {
|
||||
height: 139px;
|
||||
height: 140px;
|
||||
border: 1px solid transparent;
|
||||
border-width: 1px 0;
|
||||
border-radius: 0;
|
||||
@@ -106,12 +133,11 @@
|
||||
|
||||
.movie_result .data {
|
||||
padding: 0 15px;
|
||||
width: 440px;
|
||||
width: 470px;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
height: 140px;
|
||||
top: 0;
|
||||
margin: -143px 0 0 0;
|
||||
min-height: 140px;
|
||||
margin: -140px 0 0 0;
|
||||
background: #5c697b;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -120,7 +146,7 @@
|
||||
}
|
||||
|
||||
.movie_result:last-child .data { border-bottom: 0; }
|
||||
|
||||
|
||||
.movie_result .in_wanted, .movie_result .in_library {
|
||||
position: absolute;
|
||||
margin-top: 105px;
|
||||
@@ -131,16 +157,17 @@
|
||||
display: inline-block;
|
||||
margin: 15px 3% 15px 0;
|
||||
vertical-align: top;
|
||||
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 3px rgba(0,0,0,0.35);
|
||||
}
|
||||
|
||||
.movie_result .info {
|
||||
width: 74%;
|
||||
width: 80%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 15px 0;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.movie_result .info .tagline {
|
||||
@@ -164,3 +191,7 @@
|
||||
|
||||
.movie_result .info h2 span:before { content: "("; }
|
||||
.movie_result .info h2 span:after { content: ")"; }
|
||||
|
||||
.search_form .mask {
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -10,12 +10,23 @@ Block.Search = new Class({
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input.inlay', {
|
||||
'placeholder': 'Search for new movies',
|
||||
'placeholder': 'Search & add a new movie',
|
||||
'events': {
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': self.hideResults.bind(self, false)
|
||||
'focus': function(){
|
||||
self.el.addClass('focused')
|
||||
},
|
||||
'blur': function(){
|
||||
self.el.removeClass('focused')
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.enter', {
|
||||
'events': {
|
||||
'click': self.keyup.bind(self)
|
||||
},
|
||||
'text':'Enter'
|
||||
}),
|
||||
new Element('a', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self)
|
||||
@@ -32,24 +43,24 @@ Block.Search = new Class({
|
||||
}
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div.pointer'),
|
||||
self.results = new Element('div.results')
|
||||
).hide()
|
||||
)
|
||||
);
|
||||
|
||||
self.spinner = new Spinner(self.result_container);
|
||||
self.mask = new Element('div.mask').inject(self.result_container).fade('hide');
|
||||
|
||||
},
|
||||
|
||||
clear: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
},
|
||||
|
||||
hideResults: function(bool){
|
||||
@@ -57,7 +68,7 @@ Block.Search = new Class({
|
||||
|
||||
if(self.hidden == bool) return;
|
||||
|
||||
self.result_container[bool ? 'hide' : 'show']();
|
||||
self.el[bool ? 'removeClass' : 'addClass']('shown');
|
||||
|
||||
if(bool){
|
||||
History.removeEvent('change', self.hideResults.bind(self, !bool));
|
||||
@@ -74,16 +85,14 @@ Block.Search = new Class({
|
||||
keyup: function(e){
|
||||
var self = this;
|
||||
|
||||
if(['up', 'down'].indexOf(e.key) > -1){
|
||||
p('select item')
|
||||
}
|
||||
else if(self.q() != self.last_q) {
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
|
||||
self.autocomplete()
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
autocomplete: function(delay){
|
||||
autocomplete: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.q()){
|
||||
@@ -91,10 +100,7 @@ Block.Search = new Class({
|
||||
return
|
||||
}
|
||||
|
||||
self.spinner.show()
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.list.delay((delay || 300), self)
|
||||
self.list()
|
||||
},
|
||||
|
||||
list: function(){
|
||||
@@ -105,9 +111,14 @@ Block.Search = new Class({
|
||||
var q = self.q();
|
||||
var cache = self.cache[q];
|
||||
|
||||
self.hideResults(false)
|
||||
self.hideResults(false);
|
||||
|
||||
if(!cache){
|
||||
self.positionMask().fade('in');
|
||||
|
||||
if(!self.spinner)
|
||||
self.spinner = createSpinner(self.mask);
|
||||
|
||||
self.api_request = Api.request('movie.search', {
|
||||
'data': {
|
||||
'q': q
|
||||
@@ -125,7 +136,7 @@ Block.Search = new Class({
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.spinner.hide();
|
||||
self.positionMask()
|
||||
self.cache[q] = json
|
||||
|
||||
self.movies = {}
|
||||
@@ -138,10 +149,30 @@ Block.Search = new Class({
|
||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||
|
||||
});
|
||||
|
||||
|
||||
if(q != self.q())
|
||||
self.list()
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
|
||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
||||
self.mask.hide()
|
||||
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this;
|
||||
|
||||
var s = self.result_container.getSize()
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': self.result_container
|
||||
})
|
||||
},
|
||||
|
||||
loading: function(bool){
|
||||
@@ -252,7 +283,9 @@ Block.Search.Item = new Class({
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
Api.request('movie.add', {
|
||||
'data': {
|
||||
@@ -260,8 +293,6 @@ Block.Search.Item = new Class({
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': self.options,
|
||||
'onComplete': function(){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
@@ -293,10 +324,9 @@ Block.Search.Item = new Class({
|
||||
}) : null,
|
||||
self.info.in_wanted ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + self.info.in_wanted.label
|
||||
}) : null,
|
||||
self.info.in_library ? new Element('span.in_library', {
|
||||
}) : (self.info.in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + self.info.in_library.label
|
||||
}) : null,
|
||||
}) : null),
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
@@ -318,7 +348,7 @@ Block.Search.Item = new Class({
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
Object.each(Quality.getActiveProfiles(), function(profile){
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
@@ -330,6 +360,25 @@ Block.Search.Item = new Class({
|
||||
|
||||
},
|
||||
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
var s = self.options.getSize();
|
||||
|
||||
self.mask = new Element('span.mask', {
|
||||
'styles': {
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}
|
||||
}).inject(self.options).fade('hide').position({
|
||||
'relativeTo': self.options
|
||||
})
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* @override http://192.168.1.20:5000/static/profile_plugin/profile.css */
|
||||
|
||||
.add_new_profile {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
@@ -18,7 +16,7 @@
|
||||
padding: 14px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
|
||||
.profile .qualities {
|
||||
min-height: 80px;
|
||||
}
|
||||
@@ -42,36 +40,36 @@
|
||||
.profile .wait_for input {
|
||||
margin: 0 5px !important;
|
||||
}
|
||||
|
||||
|
||||
.profile .types {
|
||||
padding: 0;
|
||||
margin: 0 20px 0 -4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.profile .types li {
|
||||
padding: 3px 5px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
list-style: none;
|
||||
}
|
||||
.profile .types li:last-child { border: 0; }
|
||||
|
||||
|
||||
.profile .types li > * {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.profile .quality_type select {
|
||||
width: 186px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
|
||||
.profile .types li.is_empty .check, .profile .types li.is_empty .delete, .profile .types li.is_empty .handle {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.profile .types .type .handle {
|
||||
background: url('./handle.png') center;
|
||||
display: inline-block;
|
||||
@@ -82,7 +80,7 @@
|
||||
cursor: -webkit-grab;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.profile .types .type .delete {
|
||||
background-position: left center;
|
||||
height: 20px;
|
||||
@@ -90,13 +88,13 @@
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.profile .types .type:hover:not(.is_empty) .delete {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
#profile_ordering {
|
||||
|
||||
|
||||
}
|
||||
|
||||
#profile_ordering ul {
|
||||
@@ -114,19 +112,19 @@
|
||||
padding: 0 5px;
|
||||
}
|
||||
#profile_ordering li:last-child { border: 0; }
|
||||
|
||||
|
||||
#profile_ordering li .check {
|
||||
margin: 2px 10px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
#profile_ordering li > span {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
#profile_ordering li .handle {
|
||||
background: url('./handle.png') center;
|
||||
width: 20px;
|
||||
|
||||
@@ -156,7 +156,7 @@ var Profile = new Class({
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
Api.request('profile.delete', {
|
||||
'data': {
|
||||
'id': self.data.id
|
||||
|
||||
@@ -16,7 +16,7 @@ log = CPLog(__name__)
|
||||
class QualityPlugin(Plugin):
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate']},
|
||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'width': 1920, 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
||||
@@ -155,6 +155,10 @@ class QualityPlugin(Plugin):
|
||||
log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
for tag in quality.get('tags', []):
|
||||
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
if list(set(quality.get('tags', [])) & set(words)):
|
||||
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* @override http://127.0.0.1:5000/static/quality_plugin/quality.css */
|
||||
|
||||
.group_sizes {
|
||||
|
||||
|
||||
}
|
||||
|
||||
.group_sizes .head {
|
||||
@@ -17,7 +15,7 @@
|
||||
.group_sizes .label {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
|
||||
.group_sizes .min, .group_sizes .max {
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
|
||||
@@ -39,10 +39,10 @@ var QualityBase = new Class({
|
||||
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createTab('profile', {
|
||||
var tab = self.settings.createSubTab('profile', {
|
||||
'label': 'Quality',
|
||||
'name': 'profile'
|
||||
});
|
||||
}, self.settings.tabs.searcher ,'searcher');
|
||||
|
||||
self.tab = tab.tab;
|
||||
self.content = tab.content;
|
||||
@@ -93,7 +93,7 @@ var QualityBase = new Class({
|
||||
var data = data || {'id': randomString()}
|
||||
var profile = new Profile(data)
|
||||
self.profiles.include(profile)
|
||||
|
||||
|
||||
return profile;
|
||||
},
|
||||
|
||||
|
||||
@@ -16,8 +16,24 @@ class Release(Plugin):
|
||||
def __init__(self):
|
||||
addEvent('release.add', self.add)
|
||||
|
||||
addApiView('release.download', self.download)
|
||||
addApiView('release.delete', self.delete)
|
||||
addApiView('release.download', self.download, docs = {
|
||||
'desc': 'Send a release manually to the downloaders',
|
||||
'params': {
|
||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||
}
|
||||
})
|
||||
addApiView('release.delete', self.delete, docs = {
|
||||
'desc': 'Delete releases',
|
||||
'params': {
|
||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||
}
|
||||
})
|
||||
addApiView('release.ignore', self.ignore, docs = {
|
||||
'desc': 'Toggle ignore, for bad or wrong releases',
|
||||
'params': {
|
||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||
}
|
||||
})
|
||||
|
||||
def add(self, group):
|
||||
db = get_session()
|
||||
@@ -95,6 +111,22 @@ class Release(Plugin):
|
||||
'success': True
|
||||
})
|
||||
|
||||
def ignore(self):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
|
||||
def download(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -24,6 +24,7 @@ rename_options = {
|
||||
|
||||
config = [{
|
||||
'name': 'renamer',
|
||||
'order': 40,
|
||||
'description': 'Move and rename your downloaded movies to your movie directory.',
|
||||
'groups': [
|
||||
{
|
||||
|
||||
@@ -22,7 +22,9 @@ class Renamer(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('renamer.scan', self.scanView)
|
||||
addApiView('renamer.scan', self.scanView, docs = {
|
||||
'desc': 'For the renamer to check for new files to rename',
|
||||
})
|
||||
|
||||
addEvent('renamer.scan', self.scan)
|
||||
addEvent('app.load', self.scan)
|
||||
@@ -31,7 +33,7 @@ class Renamer(Plugin):
|
||||
|
||||
def scanView(self):
|
||||
|
||||
fireEvent('renamer.scan')
|
||||
fireEventAsync('renamer.scan')
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -65,6 +67,8 @@ class Renamer(Plugin):
|
||||
nfo_name = self.conf('nfo_name')
|
||||
separator = self.conf('separator')
|
||||
|
||||
db = get_session()
|
||||
|
||||
for group_identifier in groups:
|
||||
|
||||
group = groups[group_identifier]
|
||||
@@ -123,7 +127,7 @@ class Renamer(Plugin):
|
||||
# Move nfo depending on settings
|
||||
if file_type is 'nfo' and not self.conf('rename_nfo'):
|
||||
log.debug('Skipping, renaming of %s disabled' % file_type)
|
||||
if self.conf('clean_up'):
|
||||
if self.conf('cleanup'):
|
||||
for current_file in group['files'][file_type]:
|
||||
remove_files.append(current_file)
|
||||
continue
|
||||
@@ -223,7 +227,6 @@ class Renamer(Plugin):
|
||||
cd += 1
|
||||
|
||||
# Before renaming, remove the lower quality files
|
||||
db = get_session()
|
||||
|
||||
library = db.query(Library).filter_by(identifier = group['library']['identifier']).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
@@ -308,11 +311,26 @@ class Renamer(Plugin):
|
||||
if isinstance(src, File):
|
||||
src = src.path
|
||||
|
||||
log.info('(fake) Removing "%s"' % src)
|
||||
log.info('Removing "%s"' % src)
|
||||
try:
|
||||
os.remove(src)
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
|
||||
|
||||
# Remove matching releases
|
||||
for release in remove_releases:
|
||||
log.info('(fake) Removing release %s' % release.identifier)
|
||||
log.debug('Removing release %s' % release.identifier)
|
||||
try:
|
||||
db.delete(release)
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
|
||||
|
||||
if group['dirname'] and group['parentdir']:
|
||||
try:
|
||||
log.info('Deleting folder: %s' % group['parentdir'])
|
||||
self.deleteEmptyFolder(group['parentdir'])
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
||||
|
||||
# Search for trailers etc
|
||||
fireEventAsync('renamer.after', group)
|
||||
@@ -378,3 +396,20 @@ class Renamer(Plugin):
|
||||
|
||||
def replaceDoubles(self, string):
|
||||
return string.replace(' ', ' ').replace(' .', '.')
|
||||
|
||||
def deleteEmptyFolder(self, folder):
|
||||
|
||||
for root, dirs, files in os.walk(folder):
|
||||
|
||||
for dir_name in dirs:
|
||||
full_path = os.path.join(root, dir_name)
|
||||
if len(os.listdir(full_path)) == 0:
|
||||
try:
|
||||
os.rmdir(full_path)
|
||||
except:
|
||||
log.error('Couldn\'t remove empty directory %s: %s' % (full_path, traceback.format_exc()))
|
||||
|
||||
try:
|
||||
os.rmdir(folder)
|
||||
except:
|
||||
log.error('Couldn\'t remove empty directory %s: %s' % (folder, traceback.format_exc()))
|
||||
|
||||
@@ -43,7 +43,7 @@ class Scanner(Plugin):
|
||||
'trailer': ('video', 'trailer'),
|
||||
'nfo': ('nfo', 'nfo'),
|
||||
'movie': ('video', 'movie'),
|
||||
'movie': ('movie', 'movie_extra'),
|
||||
'movie_extra': ('movie', 'movie_extra'),
|
||||
'backdrop': ('image', 'backdrop'),
|
||||
'leftover': ('leftover', 'leftover'),
|
||||
}
|
||||
@@ -365,7 +365,6 @@ class Scanner(Plugin):
|
||||
def getMeta(self, filename):
|
||||
|
||||
try:
|
||||
|
||||
p = enzyme.parse(filename)
|
||||
return {
|
||||
'video': p.video[0].codec,
|
||||
@@ -377,6 +376,8 @@ class Scanner(Plugin):
|
||||
log.debug('Failed to parse meta for %s' % filename)
|
||||
except NoParserError:
|
||||
log.debug('No parser found for %s' % filename)
|
||||
except:
|
||||
log.debug('Failed parsing %s' % filename)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
|
||||
sizeScore
|
||||
sizeScore, providerScore, duplicateScore
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -31,4 +31,10 @@ class Score(Plugin):
|
||||
except:
|
||||
pass
|
||||
|
||||
# Provider score
|
||||
score += providerScore(nzb['provider'])
|
||||
|
||||
# Duplicates in name
|
||||
score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title'])
|
||||
|
||||
return score
|
||||
|
||||
@@ -27,14 +27,14 @@ def nameScore(name, year):
|
||||
score = 0
|
||||
name = name.lower()
|
||||
|
||||
#give points for the cool stuff
|
||||
# give points for the cool stuff
|
||||
for value in name_scores:
|
||||
v = value.split(':')
|
||||
add = int(v.pop())
|
||||
if v.pop() in name:
|
||||
score = score + add
|
||||
|
||||
#points if the year is correct
|
||||
# points if the year is correct
|
||||
if str(year) in name:
|
||||
score = score + 5
|
||||
|
||||
@@ -58,3 +58,24 @@ def nameRatioScore(nzb_name, movie_name):
|
||||
|
||||
def sizeScore(size):
|
||||
return 0 if size else -20
|
||||
|
||||
|
||||
def providerScore(provider):
|
||||
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
|
||||
return 30
|
||||
|
||||
if provider in ['Newznab', 'Moovee', 'X264']:
|
||||
return 10
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def duplicateScore(nzb_name, movie_name):
|
||||
|
||||
nzb_words = re.split('\W+', simplifyString(nzb_name))
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
|
||||
# minus for duplicates
|
||||
duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
|
||||
|
||||
return len(list(set(duplicates) - set(movie_words))) * -4
|
||||
|
||||
@@ -6,6 +6,7 @@ def start():
|
||||
|
||||
config = [{
|
||||
'name': 'searcher',
|
||||
'order': 20,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
|
||||
@@ -33,13 +33,19 @@ class Searcher(Plugin):
|
||||
).all()
|
||||
|
||||
for movie in movies:
|
||||
|
||||
self.single(movie.to_dict({
|
||||
movie_dict = movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
}))
|
||||
})
|
||||
|
||||
try:
|
||||
self.single(movie_dict)
|
||||
except IndexError:
|
||||
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
|
||||
except:
|
||||
log.error('Search failed for %s: %s' % (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
|
||||
@@ -20,6 +20,7 @@ class StatusPlugin(Plugin):
|
||||
'wanted': 'Wanted',
|
||||
'snatched': 'Snatched',
|
||||
'deleted': 'Deleted',
|
||||
'ignored': 'Ignored',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
@@ -29,7 +30,13 @@ class StatusPlugin(Plugin):
|
||||
addEvent('status.all', self.all)
|
||||
addEvent('app.initialize', self.fill)
|
||||
|
||||
addApiView('status.list', self.list)
|
||||
addApiView('status.list', self.list, docs = {
|
||||
'desc': 'Check for available update',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'list': array, statuses
|
||||
}"""}
|
||||
})
|
||||
|
||||
def list(self):
|
||||
|
||||
|
||||
@@ -5,7 +5,13 @@ var StatusBase = new Class({
|
||||
|
||||
self.statuses = statuses;
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
get: function(id){
|
||||
return this.statuses.filter(function(status){
|
||||
return status.id == id
|
||||
}).pick()
|
||||
},
|
||||
|
||||
});
|
||||
window.Status = new StatusBase();
|
||||
|
||||
@@ -8,7 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'renamer',
|
||||
'subtab': 'subtitles',
|
||||
'name': 'subtitle',
|
||||
'label': 'Download subtitles after rename',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -7,8 +7,10 @@ config = [{
|
||||
'name': 'trailer',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'metadata',
|
||||
'tab': 'renamer',
|
||||
'subtab': 'trailer',
|
||||
'name': 'trailer',
|
||||
'label': 'Download trailer after rename',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
var includes = {{includes|tojson}};
|
||||
var excludes = {{excludes|tojson}};
|
||||
|
||||
var specialChars = '\\{}+.():-|^$';
|
||||
var makeRegex = function(pattern) {
|
||||
pattern = pattern.split('');
|
||||
var i, len = pattern.length;
|
||||
for( i = 0; i < len; i++) {
|
||||
var character = pattern[i];
|
||||
if(specialChars.indexOf(character) > -1) {
|
||||
pattern[i] = '\\' + character;
|
||||
} else if(character === '?') {
|
||||
pattern[i] = '.';
|
||||
} else if(character === '*') {
|
||||
pattern[i] = '.*';
|
||||
}
|
||||
}
|
||||
return new RegExp('^' + pattern.join('') + '$');
|
||||
};
|
||||
|
||||
var isCorrectUrl = function() {
|
||||
for(i in includes) {
|
||||
var reg = includes[i]
|
||||
if (makeRegex(reg).test(document.location.href))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
var addUserscript = function() {
|
||||
// Add window param
|
||||
document.body.setAttribute('cp_auto_open', true)
|
||||
|
||||
// Load userscript
|
||||
var e = document.createElement('script');
|
||||
e.setAttribute('type', 'text/javascript');
|
||||
e.setAttribute('charset', 'UTF-8');
|
||||
e.setAttribute('src', '{{host}}couchpotato.js?r=' + Math.random() * 99999999);
|
||||
document.body.appendChild(e)
|
||||
}
|
||||
if(isCorrectUrl())
|
||||
addUserscript()
|
||||
else
|
||||
alert('Can\'t find a proper movie on this page..')
|
||||
@@ -1,4 +1,3 @@
|
||||
from couchpotato import index
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
@@ -8,6 +7,7 @@ from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -15,14 +15,27 @@ log = CPLog(__name__)
|
||||
|
||||
class Userscript(Plugin):
|
||||
|
||||
version = 2
|
||||
|
||||
def __init__(self):
|
||||
addApiView('userscript.get/<path:filename>', self.getUserScript, static = True)
|
||||
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
||||
addApiView('userscript', self.iFrame)
|
||||
addApiView('userscript.add_via_url', self.getViaUrl)
|
||||
addApiView('userscript.bookmark', self.bookmark)
|
||||
|
||||
addEvent('userscript.get_version', self.getVersion)
|
||||
|
||||
def getUserScript(self, filename = ''):
|
||||
def bookmark(self):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'host': getParam('host', None),
|
||||
}
|
||||
|
||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||
|
||||
def getUserScript(self, random = '', filename = ''):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
@@ -42,14 +55,14 @@ class Userscript(Plugin):
|
||||
|
||||
versions = fireEvent('userscript.get_provider_version')
|
||||
|
||||
version = 0
|
||||
version = self.version
|
||||
for v in versions:
|
||||
version += v
|
||||
|
||||
return version
|
||||
|
||||
def iFrame(self):
|
||||
return index()
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
|
||||
def getViaUrl(self):
|
||||
|
||||
|
||||
@@ -62,34 +62,54 @@ var UserscriptSettingTab = new Class({
|
||||
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createTab('userscript', {
|
||||
'label': 'Userscript',
|
||||
'name': 'userscript'
|
||||
|
||||
// See if userscript can be installed
|
||||
var userscript = false;
|
||||
try {
|
||||
if(Components.interfaces.gmIGreasemonkeyService)
|
||||
userscript = true
|
||||
}
|
||||
catch(e){
|
||||
userscript = Browser.chrome === true;
|
||||
}
|
||||
|
||||
var host_url = window.location.protocol + '//' + window.location.host;
|
||||
|
||||
self.settings.createGroup({
|
||||
'name': 'userscript',
|
||||
'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''),
|
||||
'description': 'Easily add movies via imdb.com, appletrailers and more'
|
||||
}).inject(self.settings.tabs.automation.content, 'top').adopt(
|
||||
(userscript ? [new Element('a.userscript.button', {
|
||||
'text': 'Install userscript',
|
||||
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
|
||||
'target': '_self'
|
||||
}), new Element('span.or[text=or]')] : null),
|
||||
new Element('span.bookmarklet').adopt(
|
||||
new Element('a.button.green', {
|
||||
'text': '+CouchPotato',
|
||||
'href': "javascript:void((function(){var e=document.createElement('script');e.setAttribute('type','text/javascript');e.setAttribute('charset','UTF-8');e.setAttribute('src','" +
|
||||
host_url + Api.createUrl('userscript.bookmark') +
|
||||
"?host="+ encodeURI(host_url + Api.createUrl('userscript.get')+randomString()+'/') +
|
||||
"&r='+Math.random()*99999999);document.body.appendChild(e)})());",
|
||||
'target': '',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop()
|
||||
alert('Drag it to your bookmark ;)')
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span', {
|
||||
'text': '⇽ Drag this to your bookmarks'
|
||||
})
|
||||
)
|
||||
).setStyles({
|
||||
'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
|
||||
});
|
||||
|
||||
self.tab = tab.tab;
|
||||
self.content = tab.content;
|
||||
|
||||
self.createUserscript();
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
createUserscript: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Install the Userscript'
|
||||
}).inject(self.content).adopt(
|
||||
new Element('a', {
|
||||
'text': 'Install userscript',
|
||||
'href': Api.createUrl('userscript.get')+'couchpotato.user.js',
|
||||
'target': '_self'
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -106,7 +126,7 @@ window.addEvent('load', function(){
|
||||
|
||||
if(your_version && your_version < latest_version && checked_already < latest_version){
|
||||
if(confirm("Update to the latest Userscript?\nYour version: " + your_version + ', new version: ' + latest_version )){
|
||||
document.location = Api.getOption('url')+'userscript.get/?couchpotato.user.js';
|
||||
document.location = Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js';
|
||||
}
|
||||
Cookie.write(key, latest_version, {duration: 100});
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name CouchPotato UserScript
|
||||
// @description Add movies like a real CouchPotato
|
||||
// @version {{version}}
|
||||
// @version {{version}}
|
||||
|
||||
// @match {{host}}*
|
||||
{% for include in includes %}
|
||||
@@ -57,7 +57,7 @@ if (typeof GM_addStyle == 'undefined'){
|
||||
|
||||
// Styles
|
||||
GM_addStyle('\
|
||||
#cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius-topleft: 6px; -moz-border-radius-bottomleft: 6px; -webkit-border-top-left-radius: 6px; -webkit-border-bottom-left-radius: 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \
|
||||
#cp_popup { font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; -moz-border-radius: 6px 0px 0px 6px; -webkit-border-radius: 6px 0px 0px 6px; border-radius: 6px 0px 0px 6px; -moz-box-shadow: 0 0 20px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 20px rgba(0,0,0,0.5); box-shadow: 0 0 20px rgba(0,0,0,0.5); position:fixed; z-index:9999; bottom:0; right:0; font-size:15px; margin: 20px 0; display: block; background:#4E5969; } \
|
||||
#cp_popup:hover { } \
|
||||
#cp_popup a#add_to { cursor:pointer; text-align:center; text-decoration:none; color: #000; display:block; padding:5px 0 5px 5px; } \
|
||||
#cp_popup a#close_button { cursor:pointer; float: right; padding:120px 10px 10px; } \
|
||||
@@ -72,8 +72,12 @@ var close_img = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8
|
||||
var osd = function(){
|
||||
var navbar, newElement;
|
||||
|
||||
var createApiUrl = function(url){
|
||||
return host + api + "?url=" + escape(url)
|
||||
};
|
||||
|
||||
var iframe = create('iframe', {
|
||||
'src': host + api + "?url=" + escape(document.location.href),
|
||||
'src': createApiUrl(document.location.href),
|
||||
'frameborder': 0,
|
||||
'scrolling': 'no'
|
||||
});
|
||||
@@ -81,32 +85,49 @@ var osd = function(){
|
||||
var popup = create('div', {
|
||||
'id': 'cp_popup'
|
||||
});
|
||||
|
||||
var onclick = function(){
|
||||
|
||||
// Try and get imdb url
|
||||
try {
|
||||
var regex = new RegExp(/tt(\d{7})/);
|
||||
var imdb_id = document.body.innerHTML.match(regex)[0];
|
||||
if (imdb_id)
|
||||
iframe.setAttribute('src', createApiUrl('http://imdb.com/title/'+imdb_id+'/'))
|
||||
}
|
||||
catch(e){}
|
||||
|
||||
popup.innerHTML = '';
|
||||
popup.appendChild(create('a', {
|
||||
'innerHTML': '<img src="' + close_img + '" />',
|
||||
'id': 'close_button',
|
||||
'onclick': function(){
|
||||
popup.innerHTML = '';
|
||||
popup.appendChild(add_button);
|
||||
}
|
||||
}));
|
||||
popup.appendChild(iframe)
|
||||
}
|
||||
|
||||
var add_button = create('a', {
|
||||
'innerHTML': '<img src="' + cp_icon + '" />',
|
||||
'id': 'add_to',
|
||||
'onclick': function(){
|
||||
popup.innerHTML = '';
|
||||
popup.appendChild(create('a', {
|
||||
'innerHTML': '<img src="' + close_img + '" />',
|
||||
'id': 'close_button',
|
||||
'onclick': function(){
|
||||
popup.innerHTML = '';
|
||||
popup.appendChild(add_button);
|
||||
}
|
||||
}));
|
||||
popup.appendChild(iframe)
|
||||
}
|
||||
'onclick': onclick
|
||||
});
|
||||
popup.appendChild(add_button);
|
||||
|
||||
document.body.parentNode.insertBefore(popup, document.body);
|
||||
|
||||
// Auto fold open
|
||||
if(document.body.getAttribute('cp_auto_open'))
|
||||
onclick()
|
||||
};
|
||||
|
||||
var setVersion = function(){
|
||||
document.body.setAttribute('data-userscript_version', version)
|
||||
document.body.setAttribute('data-userscript_version', version)
|
||||
};
|
||||
|
||||
if(document.location.href.indexOf(host) == -1)
|
||||
osd();
|
||||
osd();
|
||||
else
|
||||
setVersion();
|
||||
setVersion();
|
||||
@@ -1,5 +1,3 @@
|
||||
/* @override http://127.0.0.1:5000/static/wizard/wizard.css */
|
||||
|
||||
.page.wizard h1 {
|
||||
padding: 10px 30px;
|
||||
margin: 0;
|
||||
@@ -33,14 +31,14 @@
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.page.wizard .tabs li {
|
||||
display: inline-block;
|
||||
}
|
||||
.page.wizard .tabs li a {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
|
||||
.page.wizard .tab_wrapper .pointer {
|
||||
border-right: 10px solid transparent;
|
||||
border-left: 10px solid transparent;
|
||||
@@ -49,7 +47,7 @@
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
}
|
||||
|
||||
|
||||
.page.wizard .tab_content {
|
||||
margin: 20px 0 160px;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ Page.Wizard = new Class({
|
||||
},
|
||||
'finish': {
|
||||
'title': 'Finish Up',
|
||||
'description': 'Are you done? Did you fill in everything or as much as possible? Yes, ok gogogo!',
|
||||
'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!',
|
||||
'content': new Element('div').adopt(
|
||||
new Element('a.button.green', {
|
||||
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).stop();
|
||||
(e).preventDefault();
|
||||
Api.request('settings.save', {
|
||||
'data': {
|
||||
'section': 'core',
|
||||
@@ -115,7 +115,14 @@ Page.Wizard = new Class({
|
||||
if(tab_navigation && group_container){
|
||||
tab_navigation.inject(tabs); // Tab navigation
|
||||
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
|
||||
if(self.headers[group]) tab_navigation.getElement('a').set('text', (self.headers[group].label || group).capitalize());
|
||||
if(self.headers[group]){
|
||||
var a = tab_navigation.getElement('a');
|
||||
a.set('text', (self.headers[group].label || group).capitalize());
|
||||
var url_split = a.get('href').split('wizard')[1].split('/');
|
||||
if(url_split.length > 3)
|
||||
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
new Element('li.t_'+group).adopt(
|
||||
@@ -161,7 +168,7 @@ Page.Wizard = new Class({
|
||||
if(nr == 0)
|
||||
func();
|
||||
|
||||
|
||||
|
||||
var ss = new ScrollSpy( {
|
||||
min: function(){
|
||||
var c = g.getCoordinates();
|
||||
|
||||
@@ -3,21 +3,4 @@ from .main import CP
|
||||
def start():
|
||||
return CP()
|
||||
|
||||
config = [{
|
||||
'name': 'cp',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'name': 'couchpotato_automation',
|
||||
'label': 'CouchPotato',
|
||||
'description': 'Enable automatic movie adding from CouchPotato',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
config = []
|
||||
|
||||
@@ -102,4 +102,4 @@ class YarrProvider(Provider):
|
||||
return [self.cat_backup_id]
|
||||
|
||||
def found(self, new):
|
||||
log.info('Found: score(%(score)s): %(name)s' % new)
|
||||
log.info('Found: score(%(score)s) on %(provider)s: %(name)s' % new)
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
@@ -31,11 +30,7 @@ class MetaDataBase(Plugin):
|
||||
|
||||
root = self.getRootName(release)
|
||||
|
||||
try:
|
||||
movie_info = json.loads(release['library'].get('info'))
|
||||
except:
|
||||
log.error('Failed to parse movie info: %s' % traceback.format_exc())
|
||||
movie_info = {}
|
||||
movie_info = release['library'].get('info')
|
||||
|
||||
for file_type in ['nfo', 'thumbnail', 'fanart']:
|
||||
try:
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'mediabrowser',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'metadata',
|
||||
'tab': 'renamer',
|
||||
'subtab': 'metadata',
|
||||
'name': 'mediabrowser_metadata',
|
||||
'label': 'MediaBrowser',
|
||||
'description': 'Enable metadata MediaBrowser can understand',
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'sonyps3',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'metadata',
|
||||
'tab': 'renamer',
|
||||
'subtab': 'metadata',
|
||||
'name': 'sonyps3_metadata',
|
||||
'label': 'Sony PS3',
|
||||
'description': 'Enable metadata your Playstation 3 can understand',
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'wdtv',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'metadata',
|
||||
'tab': 'renamer',
|
||||
'subtab': 'metadata',
|
||||
'name': 'wdtv_metadata',
|
||||
'label': 'WDTV',
|
||||
'description': 'Enable metadata WDTV can understand',
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'xbmc',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'metadata',
|
||||
'tab': 'renamer',
|
||||
'subtab': 'metadata',
|
||||
'name': 'xbmc_metadata',
|
||||
'label': 'XBMC',
|
||||
'description': 'Enable metadata XBMC can understand',
|
||||
|
||||
@@ -48,7 +48,7 @@ class XBMC(MetaDataBase):
|
||||
pass
|
||||
|
||||
# Other values
|
||||
types = ['rating', 'year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released']
|
||||
types = ['year', 'mpaa', 'originaltitle:original_title', 'outline', 'plot', 'tagline', 'premiered:released']
|
||||
for type in types:
|
||||
|
||||
if ':' in type:
|
||||
@@ -73,7 +73,7 @@ class XBMC(MetaDataBase):
|
||||
votes.text = str(v)
|
||||
break
|
||||
except:
|
||||
log.error('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc()))
|
||||
log.debug('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc()))
|
||||
|
||||
# Genre
|
||||
for genre in movie_info.get('genres', []):
|
||||
@@ -87,9 +87,9 @@ class XBMC(MetaDataBase):
|
||||
name.text = toUnicode(actor)
|
||||
|
||||
# Directors
|
||||
for director in movie_info.get('directors', []):
|
||||
for director_name in movie_info.get('directors', []):
|
||||
director = SubElement(nfoxml, 'director')
|
||||
director.text = toUnicode(director)
|
||||
director.text = toUnicode(director_name)
|
||||
|
||||
# Writers
|
||||
for writer in movie_info.get('writers', []):
|
||||
|
||||
@@ -14,7 +14,7 @@ class IMDBAPI(MovieProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'http://www.imdbapi.com/?%s',
|
||||
'info': 'http://www.imdbapi.com/?i=%s&tomatoes=true',
|
||||
'info': 'http://www.imdbapi.com/?i=%s',
|
||||
}
|
||||
|
||||
http_time_between_calls = 0
|
||||
@@ -32,8 +32,11 @@ class IMDBAPI(MovieProvider):
|
||||
|
||||
if cached:
|
||||
result = self.parseMovie(cached)
|
||||
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
|
||||
return [result]
|
||||
if result.get('titles') and len(result.get('titles')) > 0:
|
||||
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
|
||||
return [result]
|
||||
|
||||
return []
|
||||
|
||||
return []
|
||||
|
||||
@@ -44,8 +47,9 @@ class IMDBAPI(MovieProvider):
|
||||
|
||||
if cached:
|
||||
result = self.parseMovie(cached)
|
||||
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
|
||||
return result
|
||||
if result.get('titles') and len(result.get('titles')) > 0:
|
||||
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')')
|
||||
return result
|
||||
|
||||
return {}
|
||||
|
||||
@@ -57,11 +61,19 @@ class IMDBAPI(MovieProvider):
|
||||
if isinstance(movie, (str, unicode)):
|
||||
movie = json.loads(movie)
|
||||
|
||||
if movie.get('Response') == 'Parse Error':
|
||||
return movie_data
|
||||
|
||||
tmp_movie = movie.copy()
|
||||
for key in tmp_movie:
|
||||
if tmp_movie.get(key).lower() == 'n/a':
|
||||
del movie[key]
|
||||
|
||||
movie_data = {
|
||||
'titles': [movie.get('Title', '')],
|
||||
'titles': [movie.get('Title')] if movie.get('Title') else [],
|
||||
'original_title': movie.get('Title', ''),
|
||||
'images': {
|
||||
'poster': [movie.get('Poster', '')],
|
||||
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
|
||||
},
|
||||
'rating': {
|
||||
'imdb': (tryFloat(movie.get('Rating', 0)), tryInt(movie.get('Votes', ''))),
|
||||
|
||||
@@ -10,7 +10,7 @@ config = [{
|
||||
'tab': 'providers',
|
||||
'name': 'tmdb',
|
||||
'label': 'TheMovieDB',
|
||||
'advanced': True,
|
||||
'hidden': True,
|
||||
'description': 'Used for all calls to TheMovieDB.',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -11,8 +11,8 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('movie.by_hash', self.byHash)
|
||||
addEvent('movie.search', self.search)
|
||||
addEvent('movie.info', self.getInfo)
|
||||
addEvent('movie.search', self.search, priority = 1)
|
||||
addEvent('movie.info', self.getInfo, priority = 1)
|
||||
addEvent('movie.info_by_tmdb', self.getInfoByTMDBId)
|
||||
|
||||
# Use base wrapper
|
||||
@@ -131,7 +131,7 @@ class TheMovieDb(MovieProvider):
|
||||
|
||||
# Images
|
||||
poster = self.getImage(movie, type = 'poster', size = 'cover')
|
||||
backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280')
|
||||
#backdrop = self.getImage(movie, type = 'backdrop', size = 'w1280')
|
||||
poster_original = self.getImage(movie, type = 'poster', size = 'original')
|
||||
backdrop_original = self.getImage(movie, type = 'backdrop', size = 'original')
|
||||
|
||||
@@ -152,7 +152,7 @@ class TheMovieDb(MovieProvider):
|
||||
'original_title': movie.get('original_name'),
|
||||
'images': {
|
||||
'poster': [poster] if poster else [],
|
||||
'backdrop': [backdrop] if backdrop else [],
|
||||
#'backdrop': [backdrop] if backdrop else [],
|
||||
'poster_original': [poster_original] if poster_original else [],
|
||||
'backdrop_original': [backdrop_original] if backdrop_original else [],
|
||||
},
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'moovee',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': '#alt.binaries.moovee',
|
||||
'description': 'SD movies only',
|
||||
'options': [
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'mysterbin',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'Mysterbin',
|
||||
'description': '',
|
||||
'options': [
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'newzbin',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'newzbin',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
|
||||
@@ -26,6 +26,7 @@ class Newzbin(NZBProvider, RSS):
|
||||
1024: ['r5'],
|
||||
}
|
||||
cat_ids = [
|
||||
([262144], ['bd50']),
|
||||
([2097152], ['1080p']),
|
||||
([524288], ['720p']),
|
||||
([262144], ['brrip']),
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'newznab',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'newznab',
|
||||
'description': 'Enable multiple NewzNab providers such as <a href="http://nzb.su" target="_blank">NZB.su</a>',
|
||||
'wizard': True,
|
||||
|
||||
@@ -20,8 +20,10 @@ class Newznab(NZBProvider, RSS):
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([2010], ['dvdr']),
|
||||
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||
([2040], ['720p', '1080p']),
|
||||
([2050], ['bd50']),
|
||||
]
|
||||
cat_backup_id = 2000
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'nzbclub',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'NZBClub',
|
||||
'description': '',
|
||||
'options': [
|
||||
|
||||
@@ -59,9 +59,7 @@ class NZBClub(NZBProvider, RSS):
|
||||
size = enclosure['length']
|
||||
date = self.getTextElement(nzb, "pubDate")
|
||||
|
||||
description = ''
|
||||
if 'nfo files' in self.getTextElement(nzb, "description"):
|
||||
description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000))
|
||||
description = toUnicode(self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), timeout = 25920000))
|
||||
|
||||
new = {
|
||||
'id': nzbclub_id,
|
||||
@@ -71,12 +69,16 @@ class NZBClub(NZBProvider, RSS):
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': tryInt(size) / 1024 / 1024,
|
||||
'url': enclosure['url'],
|
||||
'download': enclosure['url'],
|
||||
'download': enclosure['url'].replace(' ', '_'),
|
||||
'detail_url': self.getTextElement(nzb, "link"),
|
||||
'description': description,
|
||||
}
|
||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||
|
||||
if 'ARCHIVE inside ARCHIVE' in description:
|
||||
log.info('Wrong: Seems to be passworded files: %s' % new['name'])
|
||||
continue
|
||||
|
||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||
nzb = new, movie = movie, quality = quality,
|
||||
imdb_results = False, single_category = False, single = True)
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'nzbindex',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'nzbindex',
|
||||
'description': 'Free provider, but less accurate.',
|
||||
'options': [
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'nzbmatrix',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'nzbmatrix',
|
||||
'label': 'NZBMatrix',
|
||||
'wizard': True,
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'nzbs',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'nzbs',
|
||||
'description': 'Id and Key can be found <a href="http://nzbs.org/index.php?action=rss" target="_blank">on your nzbs.org RSS page</a>.',
|
||||
'wizard': True,
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'x264',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': '#alt.binaries.hdtv.x264',
|
||||
'description': 'HD movies only',
|
||||
'options': [
|
||||
|
||||
@@ -7,7 +7,8 @@ config = [{
|
||||
'name': 'kickasstorrents',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'providers',
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'name': 'KickAssTorrents',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ class AlloCine(UserscriptBase):
|
||||
return 'Url isn\'t from a movie'
|
||||
|
||||
try:
|
||||
data = self.urlopen(url)
|
||||
data = self.getUrl(url)
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppleTrailers(UserscriptBase):
|
||||
def getMovie(self, url):
|
||||
|
||||
try:
|
||||
data = self.urlopen(url)
|
||||
data = self.getUrl(url)
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.variable import getImdb
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import getImdb, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from urlparse import urlparse
|
||||
@@ -42,9 +43,12 @@ class UserscriptBase(Plugin):
|
||||
|
||||
return
|
||||
|
||||
def getUrl(self, url):
|
||||
return self.getCache(md5(simplifyString(url)), url = url)
|
||||
|
||||
def getMovie(self, url):
|
||||
try:
|
||||
data = self.urlopen(url)
|
||||
data = self.getUrl(url)
|
||||
except:
|
||||
data = ''
|
||||
return self.getInfo(getImdb(data))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import Letterboxd
|
||||
|
||||
def start():
|
||||
return Letterboxd()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.providers.userscript.base import UserscriptBase
|
||||
|
||||
|
||||
class Letterboxd(UserscriptBase):
|
||||
|
||||
includes = ['*://letterboxd.com/film/*']
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import RottenTomatoes
|
||||
|
||||
def start():
|
||||
return RottenTomatoes()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,19 @@
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.providers.userscript.base import UserscriptBase
|
||||
|
||||
class RottenTomatoes(UserscriptBase):
|
||||
|
||||
includes = ['*://www.rottentomatoes.com/m/*']
|
||||
|
||||
def getMovie(self, url):
|
||||
|
||||
try:
|
||||
data = self.getUrl(url)
|
||||
except:
|
||||
return
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
title = html.find('span', {'itemprop':'name'}).text
|
||||
info = fireEvent('scanner.name_year', title, single = True)
|
||||
return self.search(info['name'], info['year'])
|
||||
@@ -10,4 +10,7 @@ class TMDB(UserscriptBase):
|
||||
def getMovie(self, url):
|
||||
match = re.search('(?P<id>\d+)', url)
|
||||
movie = fireEvent('movie.info_by_tmdb', id = match.group('id'), merge = True)
|
||||
return self.getInfo(movie['imdb'])
|
||||
|
||||
if movie['imdb']:
|
||||
return self.getInfo(movie['imdb'])
|
||||
|
||||
|
||||
@@ -16,8 +16,41 @@ class Settings(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('settings', self.view)
|
||||
addApiView('settings.save', self.saveView)
|
||||
addApiView('settings', self.view, docs = {
|
||||
'desc': 'Return the options and its values of settings.conf. Including the default values and group ordering used on the settings page.',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
// objects like in __init__.py of plugin
|
||||
"options": {
|
||||
"moovee" : {
|
||||
"groups" : [{
|
||||
"description" : "SD movies only",
|
||||
"name" : "#alt.binaries.moovee",
|
||||
"options" : [{
|
||||
"default" : false,
|
||||
"name" : "enabled",
|
||||
"type" : "enabler"
|
||||
}],
|
||||
"tab" : "providers"
|
||||
}],
|
||||
"name" : "moovee"
|
||||
}
|
||||
},
|
||||
// object structured like settings.conf
|
||||
"values": {
|
||||
"moovee": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}"""}
|
||||
})
|
||||
addApiView('settings.save', self.saveView, docs = {
|
||||
'desc': 'Save setting to config file (settings.conf)',
|
||||
'params': {
|
||||
'section': {'desc': 'The section name in settings.conf'},
|
||||
'option': {'desc': 'The option name'},
|
||||
'value': {'desc': 'The value you want to save'},
|
||||
}
|
||||
})
|
||||
|
||||
def setFile(self, config_file):
|
||||
self.file = config_file
|
||||
@@ -128,7 +161,6 @@ class Settings(object):
|
||||
if not self.options.get(section_name):
|
||||
self.options[section_name] = options
|
||||
else:
|
||||
options['groups'] = self.options[section_name].get('groups') + options.get('groups')
|
||||
self.options[section_name] = mergeDicts(self.options[section_name], options)
|
||||
|
||||
def getOptions(self):
|
||||
@@ -136,7 +168,6 @@ class Settings(object):
|
||||
|
||||
|
||||
def view(self):
|
||||
|
||||
return jsonified({
|
||||
'options': self.getOptions(),
|
||||
'values': self.getValues()
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from elixir.entity import Entity
|
||||
from elixir.fields import Field
|
||||
from elixir.options import options_defaults, using_options
|
||||
from elixir.relationships import ManyToMany, OneToMany, ManyToOne
|
||||
from libs.elixir.relationships import OneToOne
|
||||
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
|
||||
String
|
||||
String, TypeDecorator
|
||||
import json
|
||||
import time
|
||||
|
||||
options_defaults["shortnames"] = True
|
||||
|
||||
@@ -15,13 +17,29 @@ options_defaults["shortnames"] = True
|
||||
# http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata
|
||||
__session__ = None
|
||||
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JsonType(TypeDecorator):
|
||||
impl = UnicodeText
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
return toUnicode(json.dumps(value, cls = SetEncoder))
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
return json.loads(value if value else '{}')
|
||||
|
||||
|
||||
class Movie(Entity):
|
||||
"""Movie Resource a movie could have multiple releases
|
||||
The files belonging to the movie object are global for the whole movie
|
||||
such as trailers, nfo, thumbnails"""
|
||||
|
||||
last_edit = Field(Integer)
|
||||
last_edit = Field(Integer, default = lambda: int(time.time()))
|
||||
|
||||
library = ManyToOne('Library')
|
||||
status = ManyToOne('Status')
|
||||
@@ -34,12 +52,11 @@ class Library(Entity):
|
||||
""""""
|
||||
|
||||
year = Field(Integer)
|
||||
identifier = Field(String(20))
|
||||
rating = Field(Float)
|
||||
identifier = Field(String(20), index = True)
|
||||
|
||||
plot = Field(UnicodeText)
|
||||
tagline = Field(UnicodeText(255))
|
||||
info = Field(UnicodeText)
|
||||
info = Field(JsonType)
|
||||
|
||||
status = ManyToOne('Status')
|
||||
movies = OneToMany('Movie')
|
||||
@@ -52,8 +69,8 @@ class LibraryTitle(Entity):
|
||||
using_options(order_by = '-default')
|
||||
|
||||
title = Field(Unicode)
|
||||
simple_title = Field(Unicode)
|
||||
default = Field(Boolean)
|
||||
simple_title = Field(Unicode, index = True)
|
||||
default = Field(Boolean, index = True)
|
||||
|
||||
language = OneToMany('Language')
|
||||
libraries = ManyToOne('Library')
|
||||
@@ -62,7 +79,7 @@ class LibraryTitle(Entity):
|
||||
class Language(Entity):
|
||||
""""""
|
||||
|
||||
identifier = Field(String(20))
|
||||
identifier = Field(String(20), index = True)
|
||||
label = Field(Unicode)
|
||||
|
||||
titles = ManyToOne('LibraryTitle')
|
||||
@@ -72,7 +89,7 @@ class Release(Entity):
|
||||
"""Logically groups all files that belong to a certain release, such as
|
||||
parts of a movie, subtitles."""
|
||||
|
||||
identifier = Field(String(100))
|
||||
identifier = Field(String(100), index = True)
|
||||
|
||||
movie = ManyToOne('Movie')
|
||||
status = ManyToOne('Status')
|
||||
@@ -85,7 +102,7 @@ class Release(Entity):
|
||||
class ReleaseInfo(Entity):
|
||||
"""Properties that can be bound to a file for off-line usage"""
|
||||
|
||||
identifier = Field(String(50))
|
||||
identifier = Field(String(50), index = True)
|
||||
value = Field(Unicode(255), nullable = False)
|
||||
|
||||
release = ManyToOne('Release')
|
||||
@@ -107,7 +124,7 @@ class Quality(Entity):
|
||||
|
||||
identifier = Field(String(20), unique = True)
|
||||
label = Field(Unicode(20))
|
||||
order = Field(Integer)
|
||||
order = Field(Integer, index = True)
|
||||
|
||||
size_min = Field(Integer)
|
||||
size_max = Field(Integer)
|
||||
@@ -121,7 +138,7 @@ class Profile(Entity):
|
||||
using_options(order_by = 'order')
|
||||
|
||||
label = Field(Unicode(50))
|
||||
order = Field(Integer)
|
||||
order = Field(Integer, index = True)
|
||||
core = Field(Boolean)
|
||||
hide = Field(Boolean)
|
||||
|
||||
@@ -133,7 +150,7 @@ class ProfileType(Entity):
|
||||
""""""
|
||||
using_options(order_by = 'order')
|
||||
|
||||
order = Field(Integer)
|
||||
order = Field(Integer, index = True)
|
||||
finish = Field(Boolean)
|
||||
wait_for = Field(Integer)
|
||||
|
||||
@@ -170,7 +187,7 @@ class FileType(Entity):
|
||||
class FileProperty(Entity):
|
||||
"""Properties that can be bound to a file for off-line usage"""
|
||||
|
||||
identifier = Field(String(20))
|
||||
identifier = Field(String(20), index = True)
|
||||
value = Field(Unicode(255), nullable = False)
|
||||
|
||||
file = ManyToOne('File')
|
||||
@@ -180,8 +197,8 @@ class History(Entity):
|
||||
"""History of actions that are connected to a certain release,
|
||||
such as, renamed to, downloaded, deleted, download subtitles etc"""
|
||||
|
||||
added = Field(Integer)
|
||||
message = Field(UnicodeText())
|
||||
added = Field(Integer, default = lambda: int(time.time()))
|
||||
message = Field(UnicodeText)
|
||||
type = Field(Unicode(50))
|
||||
|
||||
release = ManyToOne('Release')
|
||||
@@ -196,6 +213,15 @@ class RenameHistory(Entity):
|
||||
file = ManyToOne('File')
|
||||
|
||||
|
||||
class Notification(Entity):
|
||||
using_options(order_by = 'added')
|
||||
|
||||
added = Field(Integer, default = lambda: int(time.time()))
|
||||
read = Field(Boolean, default = False)
|
||||
message = Field(Unicode(255))
|
||||
data = Field(JsonType)
|
||||
|
||||
|
||||
class Folder(Entity):
|
||||
"""Renamer destination folders."""
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.loader import Loader
|
||||
from couchpotato.core.settings import Settings
|
||||
import os
|
||||
|
||||
class Env(object):
|
||||
|
||||
@@ -61,8 +62,12 @@ class Env(object):
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def getPermission(type):
|
||||
return int(Env.get('settings').get('permission_%s' % type, default = 0777))
|
||||
def getPermission(setting_type):
|
||||
perm = Env.get('settings').get('permission_%s' % setting_type, default = '0777')
|
||||
if perm[0] == '0':
|
||||
return int(perm, 8)
|
||||
else:
|
||||
return int(perm)
|
||||
|
||||
@staticmethod
|
||||
def fireEvent(*args, **kwargs):
|
||||
@@ -71,3 +76,14 @@ class Env(object):
|
||||
@staticmethod
|
||||
def addEvent(*args, **kwargs):
|
||||
return addEvent(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def getPid():
|
||||
try:
|
||||
try:
|
||||
parent = os.getppid()
|
||||
except:
|
||||
parent = None
|
||||
return '%d %s' % (os.getpid(), '(%d)' % parent if parent and parent > 1 else '')
|
||||
except:
|
||||
return 0
|
||||
|
||||
+39
-16
@@ -128,24 +128,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
|
||||
# Load migrations
|
||||
from migrate.versioning.api import version_control, db_version, version, upgrade
|
||||
db = Env.get('db_path')
|
||||
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
|
||||
logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration
|
||||
|
||||
latest_db_version = version(repo)
|
||||
|
||||
initialize = True
|
||||
try:
|
||||
current_db_version = db_version(db, repo)
|
||||
db = Env.get('db_path')
|
||||
if os.path.isfile(db.replace('sqlite:///', '')):
|
||||
initialize = False
|
||||
except:
|
||||
version_control(db, repo, version = latest_db_version)
|
||||
current_db_version = db_version(db, repo)
|
||||
|
||||
if current_db_version < latest_db_version and not debug:
|
||||
log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version))
|
||||
upgrade(db, repo)
|
||||
from migrate.versioning.api import version_control, db_version, version, upgrade
|
||||
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
|
||||
logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration
|
||||
|
||||
latest_db_version = version(repo)
|
||||
try:
|
||||
current_db_version = db_version(db, repo)
|
||||
except:
|
||||
version_control(db, repo, version = latest_db_version)
|
||||
current_db_version = db_version(db, repo)
|
||||
|
||||
if current_db_version < latest_db_version and not debug:
|
||||
log.info('Doing database upgrade. From %d to %d' % (current_db_version, latest_db_version))
|
||||
upgrade(db, repo)
|
||||
|
||||
# Configure Database
|
||||
from couchpotato.core.settings.model import setup
|
||||
@@ -185,4 +186,26 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
if fire_load: fireEventAsync('app.load')
|
||||
|
||||
# Go go go!
|
||||
app.run(**config)
|
||||
try_restart = True
|
||||
restart_tries = 5
|
||||
while try_restart:
|
||||
try:
|
||||
app.run(**config)
|
||||
except Exception, e:
|
||||
try:
|
||||
nr, msg = e
|
||||
if nr == 48:
|
||||
log.info('Already in use, try %s more time after few seconds' % restart_tries)
|
||||
time.sleep(1)
|
||||
restart_tries -= 1
|
||||
|
||||
if restart_tries > 0:
|
||||
continue
|
||||
else:
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
raise
|
||||
|
||||
try_restart = False
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user