Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud
2012-05-31 19:30:18 +02:00
38 changed files with 709 additions and 252 deletions
+37
View File
@@ -1,10 +1,39 @@
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.helpers import url_for from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect from werkzeug.utils import redirect
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
api_docs = {} api_docs = {}
api_docs_missing = [] api_docs_missing = []
api_nonblock = {}
class NonBlockHandler(RequestHandler):
stoppers = []
@asynchronous
def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route]
cls.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
def onNewMessage(self, response):
if self.request.connection.stream.closed():
return
self.finish(response)
def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers:
stop(self.onNewMessage)
cls.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs) api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
@@ -13,6 +42,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
""" Api view """ """ Api view """
def index(): def index():
index_url = url_for('web.index') index_url = url_for('web.index')
-1
View File
@@ -114,7 +114,6 @@ class Core(Plugin):
log.debug('Save to shutdown/restart') log.debug('Save to shutdown/restart')
try: try:
Env.get('httpserver').stop()
IOLoop.instance().stop() IOLoop.instance().stop()
except RuntimeError: except RuntimeError:
pass pass
+28 -10
View File
@@ -28,7 +28,7 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6) fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.check) addEvent('app.load', self.check)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
@@ -48,17 +48,20 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'} 'return': {'type': 'see updater.info'}
}) })
def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
self.updater.doUpdate()
def check(self): def check(self):
if self.isDisabled(): if self.isDisabled():
return return
if self.updater.check(): if self.updater.check():
if self.conf('automatic') and not self.updater.update_failed: if self.conf('notification') and not self.conf('automatic'):
if self.updater.doUpdate(): fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
fireEventAsync('app.restart') return True
else:
if self.conf('notification'): return False
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
def info(self): def info(self):
return self.updater.info() return self.updater.info()
@@ -67,12 +70,22 @@ class Updater(Plugin):
return jsonified(self.updater.info()) return jsonified(self.updater.info())
def checkView(self): def checkView(self):
self.check() return jsonified({
return self.updater.getInfo() 'update_available': self.check(),
'info': self.updater.info()
})
def doUpdateView(self): def doUpdateView(self):
self.check()
if not self.update_version:
log.error('Trying to update when no update is available.')
success = False
else:
success = self.updater.doUpdate()
return jsonified({ return jsonified({
'success': self.updater.doUpdate() 'success': success
}) })
@@ -137,6 +150,7 @@ class GitUpdater(BaseUpdater):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command) self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
def doUpdate(self): def doUpdate(self):
try: try:
log.debug('Stashing local changes') log.debug('Stashing local changes')
self.repo.saveStash() self.repo.saveStash()
@@ -152,6 +166,8 @@ class GitUpdater(BaseUpdater):
version_date = datetime.fromtimestamp(info['update_version']['date']) 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) fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
fireEventAsync('app.restart')
return True return True
except: except:
log.error('Failed updating via GIT: %s' % traceback.format_exc()) log.error('Failed updating via GIT: %s' % traceback.format_exc())
@@ -243,6 +259,8 @@ class SourceUpdater(BaseUpdater):
# Write update version to file # Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version)) self.createFile(self.version_file, json.dumps(self.update_version))
fireEventAsync('app.restart')
return True return True
except: except:
log.error('Failed updating: %s' % traceback.format_exc()) log.error('Failed updating: %s' % traceback.format_exc())
@@ -16,7 +16,15 @@ var UpdaterBase = new Class({
var self = this; var self = this;
Api.request('updater.check', { Api.request('updater.check', {
'onComplete': onComplete || Function.from() 'onComplete': function(json){
if(onComplete)
onComplete(json);
if(json.update_available)
self.doUpdate();
else
App.unBlockPage()
}
}) })
}, },
@@ -52,7 +60,7 @@ var UpdaterBase = new Class({
createMessage: function(data){ createMessage: function(data){
var self = this; var self = this;
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.update_version.hash; var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
if(data.update_version.changelog) if(data.update_version.changelog)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
@@ -81,13 +89,19 @@ var UpdaterBase = new Class({
Api.request('updater.update', { Api.request('updater.update', {
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating'); self.updating();
App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
} }
} }
}); });
},
updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload();
}]);
if(self.message)
self.message.destroy();
} }
}); });
+15 -9
View File
@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
is_after_event = True is_after_event = True
except: pass except: pass
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler # Return single handler
single = False single = False
try: try:
@@ -129,24 +136,23 @@ def fireEvent(name, *args, **kwargs):
if not is_after_event: if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True) fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
return results return results
except KeyError, e: except KeyError, e:
pass pass
except Exception: except Exception:
log.error('%s: %s' % (name, traceback.format_exc())) log.error('%s: %s' % (name, traceback.format_exc()))
def fireEventAsync(name, *args, **kwargs): def fireEventAsync(*args, **kwargs):
#log.debug('Async "%s": %s, %s' % (name, args, kwargs))
try: try:
e = events[name] my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
e.lock.acquire() my_thread.setDaemon(True)
e.asynchronous = True my_thread.start()
e.error_handler = errorHandler
e(*args, **kwargs)
e.lock.release()
return True return True
except Exception, e: except Exception, e:
log.error('%s: %s' % (name, e)) log.error('%s: %s' % (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
+8 -4
View File
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app from flask.globals import current_app
from flask.helpers import json from flask.helpers import json, make_response
from libs.werkzeug.urls import url_decode from libs.werkzeug.urls import url_decode
from urllib import unquote from urllib import unquote
import flask import flask
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
return getattr(current_app, 'response_class')(content, mimetype = mimetype) return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs): def jsonified(*args, **kwargs):
from couchpotato.environment import Env
callback = getParam('callback_func', None) callback = getParam('callback_func', None)
if callback: if callback:
return padded_jsonify(callback, *args, **kwargs) content = padded_jsonify(callback, *args, **kwargs)
else: else:
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs) content = jsonify('application/json', *args, **kwargs)
response = make_response(content)
response.cache_control.no_cache = True
return response
+75 -18
View File
@@ -1,6 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
@@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading
import time import time
import uuid
log = CPLog(__name__) log = CPLog(__name__)
class CoreNotifier(Notification): class CoreNotifier(Notification):
m_lock = threading.Lock()
messages = [] messages = []
listeners = []
listen_to = [ listen_to = [
'movie.downloaded', 'movie.snatched', 'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
@@ -46,14 +51,9 @@ class CoreNotifier(Notification):
}"""} }"""}
}) })
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener) 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 markAsRead(self): def markAsRead(self):
ids = [x.strip() for x in getParam('ids').split(',')] ids = [x.strip() for x in getParam('ids').split(',')]
@@ -114,25 +114,85 @@ class CoreNotifier(Notification):
ndict = n.to_dict() ndict = n.to_dict()
ndict['type'] = 'notification' ndict['type'] = 'notification'
ndict['time'] = time.time() ndict['time'] = time.time()
self.messages.append(ndict)
self.frontend(type = listener, data = data)
#db.close() #db.close()
return True return True
def frontend(self, type = 'notification', data = {}): def frontend(self, type = 'notification', data = {}):
self.messages.append({
self.m_lock.acquire()
message = {
'message_id': str(uuid.uuid4()),
'time': time.time(), 'time': time.time(),
'type': type, 'type': type,
'data': data, 'data': data,
}) }
self.messages.append(message)
while len(self.listeners) > 0 and not self.shuttingDown():
try:
listener, last_id = self.listeners.pop()
listener({
'success': True,
'result': [message],
})
except:
break
self.m_lock.release()
self.cleanMessages()
def addListener(self, callback, last_id = None):
if last_id:
messages = self.getMessages(last_id)
if len(messages) > 0:
return callback({
'success': True,
'result': messages,
})
self.listeners.append((callback, last_id))
def removeListener(self, callback):
for list_tuple in self.listeners:
try:
listener, last_id = list_tuple
if listener == callback:
self.listeners.remove(list_tuple)
except:
pass
def cleanMessages(self):
self.m_lock.acquire()
for message in self.messages:
if message['time'] < (time.time() - 15):
self.messages.remove(message)
self.m_lock.release()
def getMessages(self, last_id):
self.m_lock.acquire()
recent = []
index = 0
for i in xrange(len(self.messages)):
index = len(self.messages) - i - 1
if self.messages[index]["message_id"] == last_id: break
recent = self.messages[index:]
self.m_lock.release()
return recent or []
def listener(self): def listener(self):
messages = [] messages = []
for message in self.messages:
#delete message older then 15s
if message['time'] > (time.time() - 15):
messages.append(message)
# Get unread # Get unread
if getParam('init'): if getParam('init'):
@@ -146,9 +206,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
#db.close()
self.messages = []
return jsonified({ return jsonified({
'success': True, 'success': True,
'result': messages, 'result': messages,
@@ -8,8 +8,8 @@ var NotificationBase = new Class({
self.setOptions(options); self.setOptions(options);
// Listener // Listener
App.addEvent('load', self.startInterval.bind(self)); App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('unload', self.stopTimer.bind(self)); App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self)); App.addEvent('notification', self.notify.bind(self));
// Add test buttons to settings page // Add test buttons to settings page
@@ -30,7 +30,11 @@ var NotificationBase = new Class({
'href': App.createUrl('notifications'), 'href': App.createUrl('notifications'),
'text': 'Show older notifications' 'text': 'Show older notifications'
})); */ })); */
}) });
window.addEvent('load', function(){
self.startInterval()
});
}, },
@@ -83,37 +87,61 @@ var NotificationBase = new Class({
}, },
startInterval: function(){ startInterval: function(force){
var self = this; var self = this;
if(self.stopped && !force){
self.stopped = false;
return;
}
self.request = Api.request('notification.listener', { Api.request('notification.listener', {
'initialDelay': 100,
'delay': 3000,
'data': {'init':true}, 'data': {'init':true},
'onSuccess': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}) }).send()
self.request.startTimer()
}, },
startTimer: function(){ startPoll: function(){
if(this.request) var self = this;
this.request.startTimer()
if(self.stopped || (self.request && self.request.isRunning()))
return;
self.request = Api.request('nonblock/notification.listener', {
'onSuccess': self.processData.bind(self),
'data': {
'last_id': self.last_id
},
'onFailure': function(){
self.startPoll.delay(2000, self)
}
}).send()
}, },
stopTimer: function(){ stopPoll: function(){
if(this.request) if(this.request)
this.request.stopTimer() this.request.cancel()
this.stopped = true;
}, },
processData: function(json){ processData: function(json){
var self = this; var self = this;
self.request.options.data = {}
Array.each(json.result, function(result){ // Process data
App.fireEvent(result.type, result) if(json){
}) Array.each(json.result, function(result){
App.fireEvent(result.type, result)
})
if(json.result.length > 0)
self.last_id = json.result.getLast().message_id
}
// Restart poll
self.startPoll()
}, },
addTestButtons: function(){ addTestButtons: function(){
+2 -3
View File
@@ -1,9 +1,8 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env from couchpotato.environment import Env
from gntp import notifier from gntp import notifier
import logging
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -17,7 +16,7 @@ class Growl(Notification):
super(Growl, self).__init__() super(Growl, self).__init__()
if self.isEnabled(): if self.isEnabled():
self.register() addEvent('app.load', self.register)
def register(self): def register(self):
if self.registered: return if self.registered: return
+4 -2
View File
@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
def view(self): def view(self):
path = getParam('path', '/')
try: try:
dirs = self.getDirectories(path = getParam('path', '/'), show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except: except:
dirs = [] dirs = []
return jsonified({ return jsonified({
'is_root': getParam('path', '/') == '/', 'is_root': path == '/' or not path,
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'dirs': dirs, 'dirs': dirs,
}) })
+2 -5
View File
@@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
fireEvent('library.update_finish', data = library_dict)
#db.close()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
@@ -138,8 +135,8 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info: if not library.info:
self.update(identifier) library_dict = self.update(identifier)
dates = library.get('info', {}).get('release_dates') dates = library_dict.get('info', {}).get('release_dates')
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
+46 -12
View File
@@ -239,16 +239,18 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
for id in getParam('id').split(','): for id in getParam('id').split(','):
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = id).first()
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
if movie: if movie:
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict)) # Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
#db.close() #db.close()
return jsonified({ return jsonified({
@@ -287,6 +289,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first() m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True
do_search = False do_search = False
if not m: if not m:
m = Movie( m = Movie(
@@ -295,8 +298,14 @@ class MoviePlugin(Plugin):
status_id = status_active.get('id'), status_id = status_active.get('id'),
) )
db.add(m) db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', '')) db.commit()
do_search = True
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd: elif force_readd:
# Clean snatched history # Clean snatched history
for release in m.releases: for release in m.releases:
@@ -306,9 +315,11 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id')) m.profile_id = params.get('profile_id', default_profile.get('id'))
else: else:
log.debug('Movie already exists, not updating: %s' % params) log.debug('Movie already exists, not updating: %s' % params)
added = False
if force_readd: if force_readd:
m.status_id = status_active.get('id') m.status_id = status_active.get('id')
do_search = True
db.commit() db.commit()
@@ -321,8 +332,12 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
if (force_readd or do_search) and search_after: if do_search and search_after:
fireEventAsync('searcher.single', movie_dict) onComplete = self.createOnComplete(m.id)
onComplete()
if added:
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict)
#db.close() #db.close()
return movie_dict return movie_dict
@@ -369,7 +384,7 @@ class MoviePlugin(Plugin):
fireEvent('movie.restatus', m.id) fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict) fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
#db.close() #db.close()
return jsonified({ return jsonified({
@@ -458,3 +473,22 @@ class MoviePlugin(Plugin):
#db.close() #db.close()
return True return True
def createOnComplete(self, movie_id):
def onComplete():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
return onComplete
def createNotifyFront(self, movie_id):
def notifyFront():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
return notifyFront
+43 -19
View File
@@ -5,10 +5,12 @@ var MovieList = new Class({
options: { options: {
navigation: true, navigation: true,
limit: 50, limit: 50,
menu: [] menu: [],
add_new: false
}, },
movies: [], movies: [],
movies_added: {},
letters: {}, letters: {},
filter: { filter: {
'startswith': null, 'startswith': null,
@@ -30,6 +32,17 @@ var MovieList = new Class({
}) })
); );
self.getMovies(); self.getMovies();
if(options.add_new)
App.addEvent('movie.added', self.movieAdded.bind(self))
},
movieAdded: function(notification){
var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id])
self.createMovie(notification.data, 'top');
}, },
create: function(){ create: function(){
@@ -71,26 +84,31 @@ var MovieList = new Class({
} }
Object.each(movies, function(movie){ Object.each(movies, function(movie){
self.createMovie(movie);
// Attach proper actions
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,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list);
m.fireEvent('injected');
self.movies.include(m)
}); });
}, },
createMovie: function(movie, inject_at){
var self = this;
// Attach proper actions
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,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m)
self.movies_added[movie.id] = true;
},
createNavigation: function(){ createNavigation: function(){
var self = this; var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -260,6 +278,7 @@ var MovieList = new Class({
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
this.set('text', 'Deleting..')
Api.request('movie.delete', { Api.request('movie.delete', {
'data': { 'data': {
'id': ids.join(','), 'id': ids.join(','),
@@ -268,14 +287,19 @@ var MovieList = new Class({
'onSuccess': function(){ 'onSuccess': function(){
qObj.close(); qObj.close();
var erase_movies = [];
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()){ if (movie.isSelected()){
$(movie).destroy() $(movie).destroy()
self.movies.erase(movie) erase_movies.include(movie)
} }
}); });
self.calculateSelected() erase_movies.each(function(movie){
self.movies.erase(movie);
});
self.calculateSelected();
} }
}); });
@@ -89,7 +89,7 @@
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 64%; width: auto;
} }
.movies .info .year { .movies .info .year {
@@ -152,7 +152,6 @@
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality { .movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right; text-align: right;
float: right; float: right;
width: 30%;
} }
.movies .data .quality .available, .movies .data .quality .snatched { .movies .data .quality .available, .movies .data .quality .snatched {
@@ -200,6 +199,7 @@
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
margin: -34px 2px 0 0; margin: -34px 2px 0 0;
background: #4e5969; background: #4e5969;
position: relative;
} }
.movies .delete_container { .movies .delete_container {
+108 -40
View File
@@ -11,53 +11,121 @@ var Movie = new Class({
self.view = options.view || 'thumbs'; self.view = options.view || 'thumbs';
self.list = list; self.list = list;
self.el = new Element('div.movie.inlay');
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self));
App.addEvent('movie.busy.'+data.id, function(notification){
if(notification.data)
self.busy(true)
});
},
busy: function(set_busy){
var self = this;
if(!set_busy){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, 400);
}
}
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.el, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
s = self.el.getSize()
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': self.el
})
},
update: function(notification){
var self = this;
self.data = notification.data;
self.container.destroy();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create();
self.busy(false);
}, },
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('div.movie.inlay').adopt( self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', { self.container = new Element('div.movie_container').adopt(
'events': { self.select_checkbox = new Element('input[type=checkbox].inlay', {
'change': function(){ 'events': {
self.fireEvent('select') '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': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
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.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
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')
)
) )
); );
@@ -150,7 +218,7 @@ var Movie = new Class({
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
self.addEvent('slideEnd:once', function(){ self.addEvent('slideEnd:once', function(){
self.el.getElements('> :not(.data):not(.poster)').hide(); self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}); });
self.data_container.tween('right', -840, 0); self.data_container.tween('right', -840, 0);
@@ -221,7 +221,9 @@ Block.Search.Item = new Class({
} }
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0] 'src': info.images.poster[0],
'height': null,
'width': null
}) : null, }) : null,
new Element('div.info').adopt( new Element('div.info').adopt(
self.title = new Element('h2', { self.title = new Element('h2', {
@@ -332,8 +334,10 @@ Block.Search.Item = new Class({
self.options.adopt( self.options.adopt(
new Element('div').adopt( new Element('div').adopt(
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', { self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0] 'src': self.info.images.poster[0],
'height': null,
'width': null
}) : null, }) : null,
self.info.in_wanted ? new Element('span.in_wanted', { self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
+3 -1
View File
@@ -17,9 +17,11 @@ rename_options = {
'audio': 'Audio (DTS)', 'audio': 'Audio (DTS)',
'group': 'Releasegroup name', 'group': 'Releasegroup name',
'source': 'Source media (Bluray)', 'source': 'Source media (Bluray)',
'filename': 'Original filename', 'original': 'Original filename',
'original_folder': 'Original foldername', 'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)', 'imdb_id': 'IMDB id (tt0123456)',
'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)',
}, },
} }
+1 -2
View File
@@ -211,7 +211,7 @@ class Renamer(Plugin):
if file_type is 'subtitle': if file_type is 'subtitle':
# rename subtitles with or without language # rename subtitles with or without language
#rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name) rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
sub_langs = group['subtitle_language'].get(current_file, []) sub_langs = group['subtitle_language'].get(current_file, [])
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
@@ -314,7 +314,6 @@ class Renamer(Plugin):
break break
elif release.status_id is snatched_status.get('id'): elif release.status_id is snatched_status.get('id'):
print release.quality.label, group['meta_data']['quality']['label']
if release.quality.id is group['meta_data']['quality']['id']: if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded') log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id') release.status_id = downloaded_status.get('id')
+46 -8
View File
@@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File from couchpotato.core.settings.model import File, Movie
from couchpotato.environment import Env from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info from guessit import guess_movie_info
@@ -27,7 +27,7 @@ class Scanner(Plugin):
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
'movie_extra': ['mds'], 'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'txt', 'tag'],
@@ -161,6 +161,8 @@ class Scanner(Plugin):
except: except:
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc())) log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
db = get_session()
for file_path in files: for file_path in files:
if not os.path.exists(file_path): if not os.path.exists(file_path):
@@ -237,19 +239,51 @@ class Scanner(Plugin):
# Group the files based on the identifier # Group the files based on the identifier
for identifier, group in movie_files.iteritems(): delete_identifiers = []
for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on identifier: %s' % identifier) log.debug('Grouping files on identifier: %s' % identifier)
found_files = set(self.path_identifiers.get(identifier, [])) group = movie_files.get(identifier)
group['unsorted_files'].extend(found_files) if group:
group['unsorted_files'].extend(found_files)
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack # Remove the found files from the leftover stack
leftovers = leftovers - found_files leftovers = leftovers - set(found_files)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
# Cleaning up used
for identifier in delete_identifiers:
del self.path_identifiers[identifier]
del delete_identifiers
# Group based on folder
delete_identifiers = []
for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on foldername: %s' % identifier)
for ff in found_files:
new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
group = movie_files.get(new_identifier)
if group:
group['unsorted_files'].extend([ff])
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack
leftovers = leftovers - set([ff])
# Break if CP wants to shut down
if self.shuttingDown():
break
# Cleaning up used
for identifier in delete_identifiers:
del self.path_identifiers[identifier]
del delete_identifiers
# Determine file types # Determine file types
processed_movies = {} processed_movies = {}
@@ -327,6 +361,10 @@ class Scanner(Plugin):
group['library'] = self.determineMovie(group) group['library'] = self.determineMovie(group)
if not group['library']: if not group['library']:
log.error('Unable to determine movie: %s' % group['identifiers']) log.error('Unable to determine movie: %s' % group['identifiers'])
else:
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group processed_movies[identifier] = group
@@ -647,7 +685,7 @@ class Scanner(Plugin):
identifier = self.removeCPTag(identifier) identifier = self.removeCPTag(identifier)
# groups, release tags, scenename cleaner, regex isn't correct # groups, release tags, scenename cleaner, regex isn't correct
identifier = re.sub(self.clean, '::', simplifyString(identifier)) identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
# Year # Year
year = self.findYear(identifier) year = self.findYear(identifier)
@@ -29,7 +29,7 @@ config = [{
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored words', 'label': 'Ignored words',
'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
}, },
], ],
}, { }, {
+10 -4
View File
@@ -83,6 +83,9 @@ class Searcher(Plugin):
if not default_title: if not default_title:
return return
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True)
ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases): if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title)) log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
@@ -107,7 +110,7 @@ class Searcher(Plugin):
# Check if movie isn't deleted while searching # Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first(): if not db.query(Movie).filter_by(id = movie.get('id')).first():
return break
# Add them to this movie releases list # Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
@@ -144,7 +147,8 @@ class Searcher(Plugin):
for nzb in sorted_results: for nzb in sorted_results:
downloaded = self.download(data = nzb, movie = movie) downloaded = self.download(data = nzb, movie = movie)
if downloaded is True: if downloaded is True:
return True ret = True
break
elif downloaded != 'try_next': elif downloaded != 'try_next':
break break
else: else:
@@ -153,11 +157,13 @@ class Searcher(Plugin):
break break
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown() or ret:
break break
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close() #db.close()
return False return ret
def download(self, data, movie, manual = False): def download(self, data, movie, manual = False):
+4 -1
View File
@@ -1,5 +1,5 @@
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import os import os
@@ -17,6 +17,9 @@ class Trailer(Plugin):
if self.isDisabled() or len(group['files']['trailer']) > 0: return if self.isDisabled() or len(group['files']['trailer']) > 0: return
trailers = fireEvent('trailer.search', group = group, merge = True) trailers = fireEvent('trailer.search', group = group, merge = True)
if not trailers or trailers == []:
log.info('No trailers found for: %s' % getTitle(group['library']))
return
for trailer in trailers.get(self.conf('quality'), []): for trailer in trailers.get(self.conf('quality'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer)) destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation', 'tab': 'automation',
'name': 'imdb_automation', 'name': 'imdb_automation',
'label': 'IMDB', 'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists', 'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
'options': [ 'options': [
{ {
'name': 'automation_enabled', 'name': 'automation_enabled',
@@ -1,17 +1,17 @@
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
import StringIO
import csv
import time import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
class IMDB(Automation): class IMDB(Automation, RSS):
interval = 1800 interval = 1800
@@ -21,34 +21,41 @@ class IMDB(Automation):
return return
movies = [] movies = []
headers = {}
for csv_url in self.conf('automation_urls').split(','): enablers = self.conf('automation_urls_use').split(',')
prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
index = -1
for rss_url in self.conf('automation_urls').split(','):
index += 1
if not enablers[index]:
continue
elif 'rss.imdb' not in rss_url:
log.error('This isn\'t the correct url.: %s' % rss_url)
continue
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0)) last_update = float(Env.prop(prop_name, default = 0))
try: try:
cache_key = 'imdb_csv.%s' % md5(csv_url) cache_key = 'imdb.rss.%s' % md5(rss_url)
csv_data = self.getCache(cache_key, csv_url)
csv_reader = csv.reader(StringIO.StringIO(csv_data))
if not headers:
nr = 0
for column in csv_reader.next():
headers[column] = nr
nr += 1
for row in csv_reader: rss_data = self.getCache(cache_key, rss_url)
created = int(time.mktime(parse(row[headers['created']]).timetuple())) data = XMLTree.fromstring(rss_data)
if created < last_update: rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
imdb = getImdb(self.getTextElement(movie, "link"))
if not imdb or created < last_update:
continue continue
imdb = row[headers['const']] movies.append(imdb)
if imdb:
movies.append(imdb)
except: except:
log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc())) log.error('Failed loading IMDB watchlist: %s %s' % (rss_url, traceback.format_exc()))
Env.prop(prop_name, time.time()) Env.prop(prop_name, time.time())
return movies return movies
@@ -151,7 +151,7 @@ class TheMovieDb(MovieProvider):
movie_data = { movie_data = {
'via_tmdb': True, 'via_tmdb': True,
'id': int(movie.get('id', 0)), 'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))], 'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'), 'original_title': movie.get('original_name'),
'images': { 'images': {
@@ -81,6 +81,7 @@ class Mysterbin(NZBProvider):
'size': size, 'size': size,
'url': self.urls['download'] % myster_id, 'url': self.urls['download'] % myster_id,
'description': description, 'description': description,
'download': self.download,
'check_nzb': False, 'check_nzb': False,
} }
@@ -61,7 +61,6 @@ class Newzbin(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments) url = "%s?%s" % (self.urls['search'], arguments)
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id))) cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
single_cat = True
data = self.getCache(cache_key) data = self.getCache(cache_key)
if not data: if not data:
@@ -118,7 +117,7 @@ class Newzbin(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True) new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
@@ -71,6 +71,7 @@ class NzbIndex(NZBProvider, RSS):
'id': nzbindex_id, 'id': nzbindex_id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(), 'provider': self.getName(),
'download': self.download,
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024, 'size': tryInt(enclosure['length']) / 1024 / 1024,
-1
View File
@@ -23,7 +23,6 @@ class Env(object):
_deamonize = False _deamonize = False
_desktop = None _desktop = None
_session = None _session = None
_httpserver = None
''' Data paths and directories ''' ''' Data paths and directories '''
_app_dir = "" _app_dir = ""
+11 -9
View File
@@ -1,13 +1,13 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from couchpotato import web from couchpotato import web
from couchpotato.api import api from couchpotato.api import api, NonBlockHandler
from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.event import fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import getDataDir, tryInt from couchpotato.core.helpers.variable import getDataDir, tryInt
from logging import handlers from logging import handlers
from tornado import autoreload from tornado import autoreload
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import RequestHandler from tornado.web import RequestHandler, Application, FallbackHandler
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache from werkzeug.contrib.cache import FileSystemCache
import locale import locale
@@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Go go go! # Go go go!
web_container = WSGIContainer(app) web_container = WSGIContainer(app)
web_container._log = _log web_container._log = _log
http_server = HTTPServer(web_container, no_keep_alive = True)
Env.set('httpserver', http_server)
loop = IOLoop.instance() loop = IOLoop.instance()
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
(r'.*', FallbackHandler, dict(fallback = web_container)),
],
log_function = lambda x : None,
debug = config['use_reloader']
)
try_restart = True try_restart = True
restart_tries = 5 restart_tries = 5
while try_restart: while try_restart:
try: try:
http_server.listen(config['port'], config['host']) application.listen(config['port'], config['host'], no_keep_alive = True)
if config['use_reloader']:
autoreload.start(loop)
loop.start() loop.start()
except Exception, e: except Exception, e:
try: try:
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+15 -16
View File
@@ -24,8 +24,8 @@ var CouchPotato = new Class({
if(window.location.hash) if(window.location.hash)
History.handleInitialState(); History.handleInitialState();
else
self.openPage(window.location.pathname); self.openPage(window.location.pathname);
History.addEvent('change', self.openPage.bind(self)); History.addEvent('change', self.openPage.bind(self));
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self)); self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
@@ -211,27 +211,30 @@ var CouchPotato = new Class({
}]); }]);
}, },
checkForUpdate: function(func){ checkForUpdate: function(onComplete){
var self = this; var self = this;
Updater.check(func) Updater.check(onComplete)
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates'); self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
self.checkAvailable(3000); self.checkAvailable(3000);
}, },
checkAvailable: function(delay){ checkAvailable: function(delay, onAvailable){
var self = this; var self = this;
(function(){ (function(){
Api.request('app.available', { Api.request('app.available', {
'onFailure': function(){ 'onFailure': function(){
self.checkAvailable.delay(1000, self); self.checkAvailable.delay(1000, self, [delay, onAvailable]);
self.fireEvent('unload'); self.fireEvent('unload');
}, },
'onSuccess': function(){ 'onSuccess': function(){
if(onAvailable)
onAvailable()
self.unBlockPage(); self.unBlockPage();
self.fireEvent('reload');
} }
}); });
@@ -241,6 +244,8 @@ var CouchPotato = new Class({
blockPage: function(message, title){ blockPage: function(message, title){
var self = this; var self = this;
self.unBlockPage();
var body = $(document.body); var body = $(document.body);
self.mask = new Element('div.mask').adopt( self.mask = new Element('div.mask').adopt(
new Element('div').adopt( new Element('div').adopt(
@@ -256,20 +261,14 @@ var CouchPotato = new Class({
unBlockPage: function(){ unBlockPage: function(){
var self = this; var self = this;
self.mask.get('tween').start('opacity', 0).chain(function(){ if(self.mask)
this.element.destroy() self.mask.get('tween').start('opacity', 0).chain(function(){
}); this.element.destroy()
});
}, },
createUrl: function(action, params){ createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '') return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
},
notify: function(options){
return this.growl.notify({
title: "this scrolls away",
text: "test - hello there. mouseover to pause away action"
});
} }
}); });
+1 -1
View File
@@ -48,7 +48,7 @@ var AboutSettingTab = new Class({
'text': 'Getting version...', 'text': 'Getting version...',
'events': { 'events': {
'click': App.checkForUpdate.bind(App, function(json){ 'click': App.checkForUpdate.bind(App, function(json){
self.fillVersion(json) self.fillVersion(json.info)
}), }),
'mouseenter': function(){ 'mouseenter': function(){
this.set('text', 'Check for updates') this.set('text', 'Check for updates')
+105 -3
View File
@@ -702,7 +702,7 @@ Option.Directory = new Class({
var v = self.input.get('text'); var v = self.input.get('text');
var previous_dir = self.getParentDir(); var previous_dir = self.getParentDir();
if(previous_dir != v && previous_dir.length > 1){ if(previous_dir != v && previous_dir.length >= 1 && !json.is_root){
self.back_button.set('data-value', previous_dir) self.back_button.set('data-value', previous_dir)
self.back_button.set('html', '&laquo; '+self.getCurrentDirname(previous_dir)) self.back_button.set('html', '&laquo; '+self.getCurrentDirname(previous_dir))
self.back_button.show() self.back_button.show()
@@ -909,6 +909,7 @@ Option.Choice = new Class({
var input = self.tag_input.getElement('li:last-child input'); var input = self.tag_input.getElement('li:last-child input');
input.fireEvent('focus'); input.fireEvent('focus');
input.focus(); input.focus();
input.setCaretPosition(input.get('value').length);
} }
self.el.addEvent('outerClick', function(){ self.el.addEvent('outerClick', function(){
@@ -965,6 +966,12 @@ Option.Choice = new Class({
'onChange': self.setOrder.bind(self), 'onChange': self.setOrder.bind(self),
'onBlur': function(){ 'onBlur': function(){
self.addLastTag(); self.addLastTag();
},
'onGoLeft': function(){
self.goLeft(this)
},
'onGoRight': function(){
self.goRight(this)
} }
}); });
$(tag).inject(self.tag_input); $(tag).inject(self.tag_input);
@@ -979,6 +986,30 @@ Option.Choice = new Class({
return tag; return tag;
}, },
goLeft: function(from_tag){
var self = this;
from_tag.blur();
var prev_index = self.tags.indexOf(from_tag)-1;
if(prev_index >= 0)
self.tags[prev_index].selectFrom('right')
else
from_tag.focus();
},
goRight: function(from_tag){
var self = this;
from_tag.blur();
var next_index = self.tags.indexOf(from_tag)+1;
if(next_index < self.tags.length)
self.tags[next_index].selectFrom('left')
else
from_tag.focus();
},
setOrder: function(){ setOrder: function(){
var self = this; var self = this;
@@ -1059,7 +1090,16 @@ Option.Choice.Tag = new Class({
'width': 0 'width': 0
}, },
'events': { 'events': {
'keyup': self.is_choice ? null : function(){ 'keyup': self.is_choice ? null : function(e){
var current_caret_pos = self.input.getCaretPosition();
if(e.key == 'left' && current_caret_pos == self.last_caret_pos){
self.fireEvent('goLeft');
}
else if (e.key == 'right' && self.last_caret_pos === current_caret_pos){
self.fireEvent('goRight');
}
self.last_caret_pos = self.input.getCaretPosition();
self.setWidth(); self.setWidth();
self.fireEvent('change'); self.fireEvent('change');
}, },
@@ -1081,8 +1121,70 @@ Option.Choice.Tag = new Class({
}, },
blur: function(){
var self = this;
self.input.blur();
self.selected = false;
self.el.removeClass('selected');
self.input.removeEvents('outerClick');
},
focus: function(){ focus: function(){
this.input.focus(); var self = this;
if(!self.is_choice){
this.input.focus();
}
else {
if(self.selected) return;
self.selected = true;
self.el.addClass('selected');
self.input.addEvent('outerClick', self.blur.bind(self));
var temp_input = new Element('input', {
'events': {
'keyup': function(e){
e.stop();
if(e.key == 'right'){
self.fireEvent('goRight');
this.destroy();
}
else if (e.key == 'left'){
self.fireEvent('goLeft');
this.destroy();
}
else if (e.key == 'backspace'){
self.del();
this.destroy();
}
}
},
'styles': {
'height': 0,
'width': 0,
'position': 'absolute',
'top': -200
}
});
self.el.adopt(temp_input)
temp_input.focus();
}
},
selectFrom: function(direction){
var self = this;
if(!direction || self.is_choice){
self.focus();
}
else {
self.focus();
var position = direction == 'left' ? 0 : self.input.get('value').length;
self.input.setCaretPosition(position);
}
}, },
setWidth: function(){ setWidth: function(){
+9 -3
View File
@@ -14,10 +14,10 @@ Page.Wanted = new Class({
self.wanted = new MovieList({ self.wanted = new MovieList({
'identifier': 'wanted', 'identifier': 'wanted',
'status': 'active', 'status': 'active',
'actions': MovieActions 'actions': MovieActions,
'add_new': true
}); });
$(self.wanted).inject(self.el); $(self.wanted).inject(self.el);
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
} }
} }
@@ -73,14 +73,20 @@ window.addEvent('domready', function(){
new Element('option', { new Element('option', {
'text': alt.title 'text': alt.title
}).inject(self.title_select); }).inject(self.title_select);
if(alt['default'])
self.title_select.set('value', alt.title);
}); });
Quality.getActiveProfiles().each(function(profile){ Quality.getActiveProfiles().each(function(profile){
new Element('option', { new Element('option', {
'value': profile.id ? profile.id : profile.data.id, 'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label 'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select); }).inject(self.profile_select);
self.profile_select.set('value', (self.movie.profile || {})['id']);
if(self.movie.profile)
self.profile_select.set('value', self.movie.profile.data.id);
}); });
} }
+1
View File
@@ -437,6 +437,7 @@ body > .spinner, .mask{
border-radius:3px; border-radius:3px;
border: 1px solid #252930; border: 1px solid #252930;
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2); box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
background: rgb(55,62,74);
background-image: -webkit-gradient( background-image: -webkit-gradient(
linear, linear,
left bottom, left bottom,
+14 -7
View File
@@ -362,28 +362,29 @@
border-radius: 2px; border-radius: 2px;
} }
.page .tag_input > ul:hover > li.choice { .page .tag_input > ul:hover > li.choice {
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( background: -webkit-gradient(
linear, linear,
left bottom, left bottom,
left top, left top,
color-stop(0, rgba(255,255,255,0.1)), color-stop(0, rgba(255,255,255,0.1)),
color-stop(1, rgba(255,255,255,0.3)) color-stop(1, rgba(255,255,255,0.3))
); );
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( background: -moz-linear-gradient(
center top, center top,
rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0.1) 100% rgba(255,255,255,0.1) 100%
); );
} }
.page .tag_input > ul > li.choice:hover { .page .tag_input > ul > li.choice:hover,
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient( .page .tag_input > ul > li.choice.selected {
background: -webkit-gradient(
linear, linear,
left bottom, left bottom,
left top, left top,
color-stop(0, #406db8), color-stop(0, #406db8),
color-stop(1, #5b9bd1) color-stop(1, #5b9bd1)
); );
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient( background: -moz-linear-gradient(
center top, center top,
#5b9bd1 0%, #5b9bd1 0%,
#406db8 100% #406db8 100%
@@ -436,7 +437,8 @@
); );
background-size: 65%; background-size: 65%;
} }
.page .tag_input .choice:hover .delete { display: inline-block; } .page .tag_input .choice:hover .delete,
.page .tag_input .choice.selected .delete { display: inline-block; }
.page .tag_input .choice .delete:hover { .page .tag_input .choice .delete:hover {
height: 14px; height: 14px;
margin-top: -13px; margin-top: -13px;
@@ -587,4 +589,9 @@
.group_userscript .bookmarklet span { .group_userscript .bookmarklet span {
margin-left: 10px; margin-left: 10px;
display: inline-block; display: inline-block;
} }
.active .group_imdb_automation:not(.disabled) {
background: url('../../images/imdb_watchlist.png') no-repeat right 50px;
min-height: 210px;
}
+9 -15
View File
@@ -31,24 +31,14 @@ load_rc_config ${name}
: ${couchpotato_user:="_sabnzbd"} : ${couchpotato_user:="_sabnzbd"}
: ${couchpotato_dir:="/usr/local/couchpotato"} : ${couchpotato_dir:="/usr/local/couchpotato"}
: ${couchpotato_chdir:="${couchpotato_dir}"} : ${couchpotato_chdir:="${couchpotato_dir}"}
: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"} : ${couchpotato_pid:="/var/run/couchpotato.pid"}
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
HOST="127.0.0.1" # Set CouchPotato address here.
PORT="8081" # Set CouchPotato port here.
CPAPI="" # Set CouchPotato API key
pidfile="${couchpotato_pid}"
status_cmd="${name}_status" status_cmd="${name}_status"
stop_cmd="${name}_stop" stop_cmd="${name}_stop"
command="/usr/sbin/daemon" command="/usr/sbin/daemon"
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/couchpotato.py ${couchpotato_flags}" command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
# Check for wget and refuse to start without it.
if [ ! -x "${WGET}" ]; then
warn "couchpotato not started: You need wget to safely shut down CouchPotato."
exit 1
fi
# Ensure user is root when running this script. # Ensure user is root when running this script.
if [ `id -u` != "0" ]; then if [ `id -u` != "0" ]; then
@@ -59,19 +49,23 @@ fi
verify_couchpotato_pid() { verify_couchpotato_pid() {
# Make sure the pid corresponds to the CouchPotato process. # Make sure the pid corresponds to the CouchPotato process.
pid=`cat ${couchpotato_pid} 2>/dev/null` pid=`cat ${couchpotato_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python ${couchpotato_dir}/couchpotato.py" ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py"
return $? return $?
} }
# Try to stop CouchPotato cleanly by calling shutdown over http. # Try to stop CouchPotato cleanly by calling shutdown over http.
couchpotato_stop() { couchpotato_stop() {
echo "Stopping $name" echo "Stopping $name"
verify_couchpotato_pid verify_couchpotato_pid
${WGET} -O - -q "http://${HOST}:${PORT}/${CPAPI}/app.shutdown/" >/dev/null
if [ -n "${pid}" ]; then if [ -n "${pid}" ]; then
kill -SIGTERM ${pid} 2> /dev/null
wait_for_pids ${pid} wait_for_pids ${pid}
kill -9 ${pid} 2> /dev/null
echo "Stopped" echo "Stopped"
fi fi
} }
couchpotato_status() { couchpotato_status() {