diff --git a/couchpotato/api.py b/couchpotato/api.py
index b1dee1b3..15ef2b4c 100644
--- a/couchpotato/api.py
+++ b/couchpotato/api.py
@@ -1,10 +1,39 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
+from tornado.ioloop import IOLoop
+from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect
api = Blueprint('api', __name__)
api_docs = {}
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):
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:
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 """
def index():
index_url = url_for('web.index')
diff --git a/couchpotato/core/_base/_core/main.py b/couchpotato/core/_base/_core/main.py
index c1a24c3d..23deedfa 100644
--- a/couchpotato/core/_base/_core/main.py
+++ b/couchpotato/core/_base/_core/main.py
@@ -114,7 +114,6 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
- Env.get('httpserver').stop()
IOLoop.instance().stop()
except RuntimeError:
pass
diff --git a/couchpotato/core/_base/updater/main.py b/couchpotato/core/_base/updater/main.py
index 87716610..b88b1e9e 100644
--- a/couchpotato/core/_base/updater/main.py
+++ b/couchpotato/core/_base/updater/main.py
@@ -28,7 +28,7 @@ class Updater(Plugin):
else:
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('updater.info', self.info)
@@ -48,17 +48,20 @@ class Updater(Plugin):
'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):
if self.isDisabled():
return
if self.updater.check():
- if self.conf('automatic') and not self.updater.update_failed:
- if self.updater.doUpdate():
- fireEventAsync('app.restart')
- else:
- if self.conf('notification'):
- fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
+ if self.conf('notification') and not self.conf('automatic'):
+ fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
+ return True
+
+ return False
def info(self):
return self.updater.info()
@@ -67,12 +70,22 @@ class Updater(Plugin):
return jsonified(self.updater.info())
def checkView(self):
- self.check()
- return self.updater.getInfo()
+ return jsonified({
+ 'update_available': self.check(),
+ 'info': self.updater.info()
+ })
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({
- 'success': self.updater.doUpdate()
+ 'success': success
})
@@ -137,6 +150,7 @@ class GitUpdater(BaseUpdater):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
def doUpdate(self):
+
try:
log.debug('Stashing local changes')
self.repo.saveStash()
@@ -152,6 +166,8 @@ class GitUpdater(BaseUpdater):
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)
+ fireEventAsync('app.restart')
+
return True
except:
log.error('Failed updating via GIT: %s' % traceback.format_exc())
@@ -243,6 +259,8 @@ class SourceUpdater(BaseUpdater):
# Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version))
+ fireEventAsync('app.restart')
+
return True
except:
log.error('Failed updating: %s' % traceback.format_exc())
diff --git a/couchpotato/core/_base/updater/static/updater.js b/couchpotato/core/_base/updater/static/updater.js
index bcbf48a8..fe0a632c 100644
--- a/couchpotato/core/_base/updater/static/updater.js
+++ b/couchpotato/core/_base/updater/static/updater.js
@@ -16,7 +16,15 @@ var UpdaterBase = new Class({
var self = this;
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){
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)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
@@ -81,13 +89,19 @@ var UpdaterBase = new Class({
Api.request('updater.update', {
'onComplete': function(json){
if(json.success){
- App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
- App.checkAvailable.delay(500, App);
- if(self.message)
- self.message.destroy();
+ self.updating();
}
}
});
+ },
+
+ 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();
}
});
diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py
index 82f81e21..b7bed808 100644
--- a/couchpotato/core/event.py
+++ b/couchpotato/core/event.py
@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
is_after_event = True
except: pass
+ # onComplete event
+ on_complete = False
+ try:
+ on_complete = kwargs['on_complete']
+ del kwargs['on_complete']
+ except: pass
+
# Return single handler
single = False
try:
@@ -129,24 +136,23 @@ def fireEvent(name, *args, **kwargs):
if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True)
+ if on_complete:
+ on_complete()
+
return results
except KeyError, e:
pass
except Exception:
log.error('%s: %s' % (name, traceback.format_exc()))
-def fireEventAsync(name, *args, **kwargs):
- #log.debug('Async "%s": %s, %s' % (name, args, kwargs))
+def fireEventAsync(*args, **kwargs):
try:
- e = events[name]
- e.lock.acquire()
- e.asynchronous = True
- e.error_handler = errorHandler
- e(*args, **kwargs)
- e.lock.release()
+ my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
+ my_thread.setDaemon(True)
+ my_thread.start()
return True
except Exception, e:
- log.error('%s: %s' % (name, e))
+ log.error('%s: %s' % (args[0], e))
def errorHandler(error):
etype, value, tb = error
diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py
index 7ff4bb29..07aa18e8 100644
--- a/couchpotato/core/helpers/request.py
+++ b/couchpotato/core/helpers/request.py
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp
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 urllib import unquote
import flask
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs):
- from couchpotato.environment import Env
callback = getParam('callback_func', None)
if callback:
- return padded_jsonify(callback, *args, **kwargs)
+ content = padded_jsonify(callback, *args, **kwargs)
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
diff --git a/couchpotato/core/notifications/core/main.py b/couchpotato/core/notifications/core/main.py
index 10a1401d..e2c7c6a0 100644
--- a/couchpotato/core/notifications/core/main.py
+++ b/couchpotato/core/notifications/core/main.py
@@ -1,6 +1,6 @@
from couchpotato import get_session
-from couchpotato.api import addApiView
-from couchpotato.core.event import addEvent, fireEvent
+from couchpotato.api import addApiView, addNonBlockApiView
+from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
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.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_
+import threading
import time
+import uuid
log = CPLog(__name__)
class CoreNotifier(Notification):
+ m_lock = threading.Lock()
messages = []
+ listeners = []
+
listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
@@ -46,14 +51,9 @@ class CoreNotifier(Notification):
}"""}
})
+ addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
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):
ids = [x.strip() for x in getParam('ids').split(',')]
@@ -114,25 +114,85 @@ class CoreNotifier(Notification):
ndict = n.to_dict()
ndict['type'] = 'notification'
ndict['time'] = time.time()
- self.messages.append(ndict)
+
+ self.frontend(type = listener, data = data)
#db.close()
return True
def frontend(self, type = 'notification', data = {}):
- self.messages.append({
+
+ self.m_lock.acquire()
+ message = {
+ 'message_id': str(uuid.uuid4()),
'time': time.time(),
'type': type,
'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):
messages = []
- for message in self.messages:
- #delete message older then 15s
- if message['time'] > (time.time() - 15):
- messages.append(message)
# Get unread
if getParam('init'):
@@ -146,9 +206,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification'
messages.append(ndict)
- #db.close()
-
- self.messages = []
return jsonified({
'success': True,
'result': messages,
diff --git a/couchpotato/core/notifications/core/static/notification.js b/couchpotato/core/notifications/core/static/notification.js
index 371b95ed..d11b6400 100644
--- a/couchpotato/core/notifications/core/static/notification.js
+++ b/couchpotato/core/notifications/core/static/notification.js
@@ -8,8 +8,8 @@ var NotificationBase = new Class({
self.setOptions(options);
// Listener
- App.addEvent('load', self.startInterval.bind(self));
- App.addEvent('unload', self.stopTimer.bind(self));
+ App.addEvent('unload', self.stopPoll.bind(self));
+ App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self));
// Add test buttons to settings page
@@ -30,7 +30,11 @@ var NotificationBase = new Class({
'href': App.createUrl('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;
+
+ if(self.stopped && !force){
+ self.stopped = false;
+ return;
+ }
- self.request = Api.request('notification.listener', {
- 'initialDelay': 100,
- 'delay': 3000,
+ Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
- })
-
- self.request.startTimer()
+ }).send()
},
- startTimer: function(){
- if(this.request)
- this.request.startTimer()
+ startPoll: function(){
+ var self = this;
+
+ 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)
- this.request.stopTimer()
+ this.request.cancel()
+ this.stopped = true;
},
processData: function(json){
var self = this;
- self.request.options.data = {}
- Array.each(json.result, function(result){
- App.fireEvent(result.type, result)
- })
+
+ // Process data
+ 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(){
diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py
index b98888e3..72ba2a56 100644
--- a/couchpotato/core/notifications/growl/main.py
+++ b/couchpotato/core/notifications/growl/main.py
@@ -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.notifications.base import Notification
from couchpotato.environment import Env
from gntp import notifier
-import logging
import traceback
log = CPLog(__name__)
@@ -17,7 +16,7 @@ class Growl(Notification):
super(Growl, self).__init__()
if self.isEnabled():
- self.register()
+ addEvent('app.load', self.register)
def register(self):
if self.registered: return
diff --git a/couchpotato/core/plugins/browser/main.py b/couchpotato/core/plugins/browser/main.py
index 887edc30..90f2673c 100644
--- a/couchpotato/core/plugins/browser/main.py
+++ b/couchpotato/core/plugins/browser/main.py
@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
def view(self):
+ path = getParam('path', '/')
+
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:
dirs = []
return jsonified({
- 'is_root': getParam('path', '/') == '/',
+ 'is_root': path == '/' or not path,
'empty': len(dirs) == 0,
'dirs': dirs,
})
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
index 347d8580..741f48bd 100644
--- a/couchpotato/core/plugins/library/main.py
+++ b/couchpotato/core/plugins/library/main.py
@@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict)
- fireEvent('library.update_finish', data = library_dict)
-
- #db.close()
return library_dict
def updateReleaseDate(self, identifier):
@@ -138,8 +135,8 @@ class LibraryPlugin(Plugin):
library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info:
- self.update(identifier)
- dates = library.get('info', {}).get('release_dates')
+ library_dict = self.update(identifier)
+ dates = library_dict.get('info', {}).get('release_dates')
else:
dates = library.info.get('release_date')
diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py
index 2c5ec731..bcd5d63e 100644
--- a/couchpotato/core/plugins/movie/main.py
+++ b/couchpotato/core/plugins/movie/main.py
@@ -239,16 +239,18 @@ class MoviePlugin(Plugin):
db = get_session()
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()
- # Get current selected title
- default_title = ''
- for title in movie.library.titles:
- if title.default: default_title = title.title
-
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()
return jsonified({
@@ -287,6 +289,7 @@ class MoviePlugin(Plugin):
db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
+ added = True
do_search = False
if not m:
m = Movie(
@@ -295,8 +298,14 @@ class MoviePlugin(Plugin):
status_id = status_active.get('id'),
)
db.add(m)
- fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
- do_search = True
+ db.commit()
+
+ 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:
# Clean snatched history
for release in m.releases:
@@ -306,9 +315,11 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id'))
else:
log.debug('Movie already exists, not updating: %s' % params)
+ added = False
if force_readd:
m.status_id = status_active.get('id')
+ do_search = True
db.commit()
@@ -321,8 +332,12 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict)
- if (force_readd or do_search) and search_after:
- fireEventAsync('searcher.single', movie_dict)
+ if do_search and search_after:
+ onComplete = self.createOnComplete(m.id)
+ onComplete()
+
+ if added:
+ fireEvent('notify.frontend', type = 'movie.added', data = movie_dict)
#db.close()
return movie_dict
@@ -369,7 +384,7 @@ class MoviePlugin(Plugin):
fireEvent('movie.restatus', m.id)
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()
return jsonified({
@@ -458,3 +473,22 @@ class MoviePlugin(Plugin):
#db.close()
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
diff --git a/couchpotato/core/plugins/movie/static/list.js b/couchpotato/core/plugins/movie/static/list.js
index 23bdcf89..52afbc20 100644
--- a/couchpotato/core/plugins/movie/static/list.js
+++ b/couchpotato/core/plugins/movie/static/list.js
@@ -5,10 +5,12 @@ var MovieList = new Class({
options: {
navigation: true,
limit: 50,
- menu: []
+ menu: [],
+ add_new: false
},
movies: [],
+ movies_added: {},
letters: {},
filter: {
'startswith': null,
@@ -30,6 +32,17 @@ var MovieList = new Class({
})
);
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(){
@@ -71,26 +84,31 @@ var MovieList = new Class({
}
Object.each(movies, function(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)
-
+ self.createMovie(movie);
});
},
+ 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(){
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -260,6 +278,7 @@ var MovieList = new Class({
'events': {
'click': function(e){
(e).preventDefault();
+ this.set('text', 'Deleting..')
Api.request('movie.delete', {
'data': {
'id': ids.join(','),
@@ -268,14 +287,19 @@ var MovieList = new Class({
'onSuccess': function(){
qObj.close();
+ var erase_movies = [];
self.movies.each(function(movie){
if (movie.isSelected()){
$(movie).destroy()
- self.movies.erase(movie)
+ erase_movies.include(movie)
}
});
- self.calculateSelected()
+ erase_movies.each(function(movie){
+ self.movies.erase(movie);
+ });
+
+ self.calculateSelected();
}
});
diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css
index 6365f798..f626f2ab 100644
--- a/couchpotato/core/plugins/movie/static/movie.css
+++ b/couchpotato/core/plugins/movie/static/movie.css
@@ -89,7 +89,7 @@
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
- width: 64%;
+ width: auto;
}
.movies .info .year {
@@ -152,7 +152,6 @@
.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 {
@@ -200,6 +199,7 @@
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
margin: -34px 2px 0 0;
background: #4e5969;
+ position: relative;
}
.movies .delete_container {
diff --git a/couchpotato/core/plugins/movie/static/movie.js b/couchpotato/core/plugins/movie/static/movie.js
index 39b12eed..9877b12f 100644
--- a/couchpotato/core/plugins/movie/static/movie.js
+++ b/couchpotato/core/plugins/movie/static/movie.js
@@ -11,53 +11,121 @@ var Movie = new Class({
self.view = options.view || 'thumbs';
self.list = list;
+ self.el = new Element('div.movie.inlay');
+
self.profile = Quality.getProfile(data.profile_id) || {};
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(){
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': {
- 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.el.adopt(
+ self.container = new Element('div.movie_container').adopt(
+ self.select_checkbox = new Element('input[type=checkbox].inlay', {
+ 'events': {
+ 'change': function(){
+ self.fireEvent('select')
}
- })
- ),
- 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.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);
diff --git a/couchpotato/core/plugins/movie/static/search.js b/couchpotato/core/plugins/movie/static/search.js
index 438ba9aa..bfb3b0d4 100644
--- a/couchpotato/core/plugins/movie/static/search.js
+++ b/couchpotato/core/plugins/movie/static/search.js
@@ -221,7 +221,9 @@ Block.Search.Item = new Class({
}
}).adopt(
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,
new Element('div.info').adopt(
self.title = new Element('h2', {
@@ -332,8 +334,10 @@ Block.Search.Item = new Class({
self.options.adopt(
new Element('div').adopt(
- self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
- 'src': self.info.images.poster[0]
+ self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
+ 'src': self.info.images.poster[0],
+ 'height': null,
+ 'width': null
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py
index d7e78511..9ceccb8b 100644
--- a/couchpotato/core/plugins/renamer/__init__.py
+++ b/couchpotato/core/plugins/renamer/__init__.py
@@ -17,9 +17,11 @@ rename_options = {
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',
'source': 'Source media (Bluray)',
- 'filename': 'Original filename',
+ 'original': 'Original filename',
'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)',
+ 'cd': 'CD number (cd1)',
+ 'cd_nr': 'Just the cd nr. (1)',
},
}
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index 53310c47..d33ba89f 100644
--- a/couchpotato/core/plugins/renamer/main.py
+++ b/couchpotato/core/plugins/renamer/main.py
@@ -211,7 +211,7 @@ class Renamer(Plugin):
if file_type is 'subtitle':
# 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, [])
rename_extras = self.getRenameExtras(
@@ -314,7 +314,6 @@ class Renamer(Plugin):
break
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']:
log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id')
diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py
index e287fd33..6fa3f7d5 100644
--- a/couchpotato/core/plugins/scanner/main.py
+++ b/couchpotato/core/plugins/scanner/main.py
@@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
from couchpotato.core.logger import CPLog
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 enzyme.exceptions import NoParserError, ParseError
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
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
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'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
@@ -161,6 +161,8 @@ class Scanner(Plugin):
except:
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
+ db = get_session()
+
for file_path in files:
if not os.path.exists(file_path):
@@ -237,19 +239,51 @@ class Scanner(Plugin):
# 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)
- found_files = set(self.path_identifiers.get(identifier, []))
- group['unsorted_files'].extend(found_files)
+ group = movie_files.get(identifier)
+ if group:
+ group['unsorted_files'].extend(found_files)
+ delete_identifiers.append(identifier)
- # Remove the found files from the leftover stack
- leftovers = leftovers - found_files
+ # Remove the found files from the leftover stack
+ leftovers = leftovers - set(found_files)
# 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
+
+ # 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
processed_movies = {}
@@ -327,6 +361,10 @@ class Scanner(Plugin):
group['library'] = self.determineMovie(group)
if not group['library']:
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
@@ -647,7 +685,7 @@ class Scanner(Plugin):
identifier = self.removeCPTag(identifier)
# 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 = self.findYear(identifier)
diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py
index a3f88555..7ecd29b7 100644
--- a/couchpotato/core/plugins/searcher/__init__.py
+++ b/couchpotato/core/plugins/searcher/__init__.py
@@ -29,7 +29,7 @@ config = [{
{
'name': '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',
},
],
}, {
diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py
index 03d9261f..93eb55ed 100644
--- a/couchpotato/core/plugins/searcher/main.py
+++ b/couchpotato/core/plugins/searcher/main.py
@@ -83,6 +83,9 @@ class Searcher(Plugin):
if not default_title:
return
+ fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True)
+
+ ret = False
for quality_type in movie['profile']['types']:
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))
@@ -107,7 +110,7 @@ class Searcher(Plugin):
# Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first():
- return
+ break
# Add them to this movie releases list
for nzb in sorted_results:
@@ -144,7 +147,8 @@ class Searcher(Plugin):
for nzb in sorted_results:
downloaded = self.download(data = nzb, movie = movie)
if downloaded is True:
- return True
+ ret = True
+ break
elif downloaded != 'try_next':
break
else:
@@ -153,11 +157,13 @@ class Searcher(Plugin):
break
# Break if CP wants to shut down
- if self.shuttingDown():
+ if self.shuttingDown() or ret:
break
+ fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
+
#db.close()
- return False
+ return ret
def download(self, data, movie, manual = False):
diff --git a/couchpotato/core/plugins/trailer/main.py b/couchpotato/core/plugins/trailer/main.py
index 8f8e4ab2..7c6d5d3f 100644
--- a/couchpotato/core/plugins/trailer/main.py
+++ b/couchpotato/core/plugins/trailer/main.py
@@ -1,5 +1,5 @@
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.plugins.base import Plugin
import os
@@ -17,6 +17,9 @@ class Trailer(Plugin):
if self.isDisabled() or len(group['files']['trailer']) > 0: return
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'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
diff --git a/couchpotato/core/providers/automation/imdb/__init__.py b/couchpotato/core/providers/automation/imdb/__init__.py
index 338a8b62..925138d0 100644
--- a/couchpotato/core/providers/automation/imdb/__init__.py
+++ b/couchpotato/core/providers/automation/imdb/__init__.py
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation',
'name': 'imdb_automation',
'label': 'IMDB',
- 'description': 'From any public IMDB watchlists',
+ 'description': 'From any public IMDB watchlists. Url should be the RSS link.',
'options': [
{
'name': 'automation_enabled',
diff --git a/couchpotato/core/providers/automation/imdb/main.py b/couchpotato/core/providers/automation/imdb/main.py
index 02fde26b..6364a75f 100644
--- a/couchpotato/core/providers/automation/imdb/main.py
+++ b/couchpotato/core/providers/automation/imdb/main.py
@@ -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.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
-import StringIO
-import csv
import time
import traceback
+import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
-class IMDB(Automation):
+class IMDB(Automation, RSS):
interval = 1800
@@ -21,34 +21,41 @@ class IMDB(Automation):
return
movies = []
- headers = {}
- for csv_url in self.conf('automation_urls').split(','):
- prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
+ enablers = self.conf('automation_urls_use').split(',')
+
+ 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))
try:
- cache_key = 'imdb_csv.%s' % md5(csv_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
+ cache_key = 'imdb.rss.%s' % md5(rss_url)
- for row in csv_reader:
- created = int(time.mktime(parse(row[headers['created']]).timetuple()))
- if created < last_update:
+ rss_data = self.getCache(cache_key, rss_url)
+ data = XMLTree.fromstring(rss_data)
+ 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
- imdb = row[headers['const']]
- if imdb:
- movies.append(imdb)
+ movies.append(imdb)
+
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())
-
return movies
diff --git a/couchpotato/core/providers/movie/themoviedb/main.py b/couchpotato/core/providers/movie/themoviedb/main.py
index 387fcac4..b1a1b62b 100644
--- a/couchpotato/core/providers/movie/themoviedb/main.py
+++ b/couchpotato/core/providers/movie/themoviedb/main.py
@@ -151,7 +151,7 @@ class TheMovieDb(MovieProvider):
movie_data = {
'via_tmdb': True,
- 'id': int(movie.get('id', 0)),
+ 'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'images': {
diff --git a/couchpotato/core/providers/nzb/mysterbin/main.py b/couchpotato/core/providers/nzb/mysterbin/main.py
index 902c37c5..5e619792 100644
--- a/couchpotato/core/providers/nzb/mysterbin/main.py
+++ b/couchpotato/core/providers/nzb/mysterbin/main.py
@@ -81,6 +81,7 @@ class Mysterbin(NZBProvider):
'size': size,
'url': self.urls['download'] % myster_id,
'description': description,
+ 'download': self.download,
'check_nzb': False,
}
diff --git a/couchpotato/core/providers/nzb/newzbin/main.py b/couchpotato/core/providers/nzb/newzbin/main.py
index c6de2a29..9239e84f 100644
--- a/couchpotato/core/providers/nzb/newzbin/main.py
+++ b/couchpotato/core/providers/nzb/newzbin/main.py
@@ -61,7 +61,6 @@ class Newzbin(NZBProvider, RSS):
url = "%s?%s" % (self.urls['search'], arguments)
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
- single_cat = True
data = self.getCache(cache_key)
if not data:
@@ -118,7 +117,7 @@ class Newzbin(NZBProvider, RSS):
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
- imdb_results = True, single_category = single_cat, single = True)
+ imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
diff --git a/couchpotato/core/providers/nzb/nzbindex/main.py b/couchpotato/core/providers/nzb/nzbindex/main.py
index 43364aca..fd53cdde 100644
--- a/couchpotato/core/providers/nzb/nzbindex/main.py
+++ b/couchpotato/core/providers/nzb/nzbindex/main.py
@@ -71,6 +71,7 @@ class NzbIndex(NZBProvider, RSS):
'id': nzbindex_id,
'type': 'nzb',
'provider': self.getName(),
+ 'download': self.download,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
diff --git a/couchpotato/environment.py b/couchpotato/environment.py
index a6f3ebb3..e804170d 100644
--- a/couchpotato/environment.py
+++ b/couchpotato/environment.py
@@ -23,7 +23,6 @@ class Env(object):
_deamonize = False
_desktop = None
_session = None
- _httpserver = None
''' Data paths and directories '''
_app_dir = ""
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index 280ef758..35a3bf96 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -1,13 +1,13 @@
from argparse import ArgumentParser
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.helpers.variable import getDataDir, tryInt
from logging import handlers
from tornado import autoreload
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
-from tornado.web import RequestHandler
+from tornado.web import RequestHandler, Application, FallbackHandler
from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache
import locale
@@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Go go go!
web_container = WSGIContainer(app)
web_container._log = _log
- http_server = HTTPServer(web_container, no_keep_alive = True)
- Env.set('httpserver', http_server)
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
restart_tries = 5
while try_restart:
try:
- http_server.listen(config['port'], config['host'])
-
- if config['use_reloader']:
- autoreload.start(loop)
-
+ application.listen(config['port'], config['host'], no_keep_alive = True)
loop.start()
except Exception, e:
try:
diff --git a/couchpotato/static/images/imdb_watchlist.png b/couchpotato/static/images/imdb_watchlist.png
new file mode 100644
index 00000000..fc4158bd
Binary files /dev/null and b/couchpotato/static/images/imdb_watchlist.png differ
diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js
index e2c23b82..b983fb5a 100644
--- a/couchpotato/static/scripts/couchpotato.js
+++ b/couchpotato/static/scripts/couchpotato.js
@@ -24,8 +24,8 @@ var CouchPotato = new Class({
if(window.location.hash)
History.handleInitialState();
- else
- self.openPage(window.location.pathname);
+
+ self.openPage(window.location.pathname);
History.addEvent('change', self.openPage.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;
- Updater.check(func)
+ Updater.check(onComplete)
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
self.checkAvailable(3000);
},
- checkAvailable: function(delay){
+ checkAvailable: function(delay, onAvailable){
var self = this;
(function(){
Api.request('app.available', {
'onFailure': function(){
- self.checkAvailable.delay(1000, self);
+ self.checkAvailable.delay(1000, self, [delay, onAvailable]);
self.fireEvent('unload');
},
'onSuccess': function(){
+ if(onAvailable)
+ onAvailable()
self.unBlockPage();
+ self.fireEvent('reload');
}
});
@@ -241,6 +244,8 @@ var CouchPotato = new Class({
blockPage: function(message, title){
var self = this;
+ self.unBlockPage();
+
var body = $(document.body);
self.mask = new Element('div.mask').adopt(
new Element('div').adopt(
@@ -256,20 +261,14 @@ var CouchPotato = new Class({
unBlockPage: function(){
var self = this;
- self.mask.get('tween').start('opacity', 0).chain(function(){
- this.element.destroy()
- });
+ if(self.mask)
+ self.mask.get('tween').start('opacity', 0).chain(function(){
+ this.element.destroy()
+ });
},
createUrl: function(action, params){
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
- },
-
- notify: function(options){
- return this.growl.notify({
- title: "this scrolls away",
- text: "test - hello there. mouseover to pause away action"
- });
}
});
diff --git a/couchpotato/static/scripts/page/about.js b/couchpotato/static/scripts/page/about.js
index ad0dd5b9..93687b49 100644
--- a/couchpotato/static/scripts/page/about.js
+++ b/couchpotato/static/scripts/page/about.js
@@ -48,7 +48,7 @@ var AboutSettingTab = new Class({
'text': 'Getting version...',
'events': {
'click': App.checkForUpdate.bind(App, function(json){
- self.fillVersion(json)
+ self.fillVersion(json.info)
}),
'mouseenter': function(){
this.set('text', 'Check for updates')
diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js
index 20ca457e..8bf04053 100644
--- a/couchpotato/static/scripts/page/settings.js
+++ b/couchpotato/static/scripts/page/settings.js
@@ -702,7 +702,7 @@ Option.Directory = new Class({
var v = self.input.get('text');
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('html', '« '+self.getCurrentDirname(previous_dir))
self.back_button.show()
@@ -909,6 +909,7 @@ Option.Choice = new Class({
var input = self.tag_input.getElement('li:last-child input');
input.fireEvent('focus');
input.focus();
+ input.setCaretPosition(input.get('value').length);
}
self.el.addEvent('outerClick', function(){
@@ -965,6 +966,12 @@ Option.Choice = new Class({
'onChange': self.setOrder.bind(self),
'onBlur': function(){
self.addLastTag();
+ },
+ 'onGoLeft': function(){
+ self.goLeft(this)
+ },
+ 'onGoRight': function(){
+ self.goRight(this)
}
});
$(tag).inject(self.tag_input);
@@ -979,6 +986,30 @@ Option.Choice = new Class({
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(){
var self = this;
@@ -1059,7 +1090,16 @@ Option.Choice.Tag = new Class({
'width': 0
},
'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.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(){
- 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(){
diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js
index 1f811c69..26d04666 100644
--- a/couchpotato/static/scripts/page/wanted.js
+++ b/couchpotato/static/scripts/page/wanted.js
@@ -14,10 +14,10 @@ Page.Wanted = new Class({
self.wanted = new MovieList({
'identifier': 'wanted',
'status': 'active',
- 'actions': MovieActions
+ 'actions': MovieActions,
+ 'add_new': true
});
$(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', {
'text': alt.title
}).inject(self.title_select);
+
+ if(alt['default'])
+ self.title_select.set('value', alt.title);
});
+
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.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);
});
}
diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css
index d580368c..884d8724 100644
--- a/couchpotato/static/style/main.css
+++ b/couchpotato/static/style/main.css
@@ -437,6 +437,7 @@ body > .spinner, .mask{
border-radius:3px;
border: 1px solid #252930;
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
+ background: rgb(55,62,74);
background-image: -webkit-gradient(
linear,
left bottom,
diff --git a/couchpotato/static/style/page/settings.css b/couchpotato/static/style/page/settings.css
index 44390f95..59a03c82 100644
--- a/couchpotato/static/style/page/settings.css
+++ b/couchpotato/static/style/page/settings.css
@@ -362,28 +362,29 @@
border-radius: 2px;
}
.page .tag_input > ul:hover > li.choice {
- background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
+ background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, rgba(255,255,255,0.1)),
color-stop(1, rgba(255,255,255,0.3))
);
- background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
+ background: -moz-linear-gradient(
center top,
rgba(255,255,255,0.3) 0%,
rgba(255,255,255,0.1) 100%
);
}
- .page .tag_input > ul > li.choice:hover {
- background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
+ .page .tag_input > ul > li.choice:hover,
+ .page .tag_input > ul > li.choice.selected {
+ background: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0, #406db8),
color-stop(1, #5b9bd1)
);
- background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
+ background: -moz-linear-gradient(
center top,
#5b9bd1 0%,
#406db8 100%
@@ -436,7 +437,8 @@
);
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 {
height: 14px;
margin-top: -13px;
@@ -587,4 +589,9 @@
.group_userscript .bookmarklet span {
margin-left: 10px;
display: inline-block;
- }
\ No newline at end of file
+ }
+
+.active .group_imdb_automation:not(.disabled) {
+ background: url('../../images/imdb_watchlist.png') no-repeat right 50px;
+ min-height: 210px;
+}
\ No newline at end of file
diff --git a/init/freebsd b/init/freebsd
index 770e22cd..e3cf408e 100644
--- a/init/freebsd
+++ b/init/freebsd
@@ -31,24 +31,14 @@ load_rc_config ${name}
: ${couchpotato_user:="_sabnzbd"}
: ${couchpotato_dir:="/usr/local/couchpotato"}
: ${couchpotato_chdir:="${couchpotato_dir}"}
-: ${couchpotato_pid:="${couchpotato_dir}/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
+: ${couchpotato_pid:="/var/run/couchpotato.pid"}
+pidfile="${couchpotato_pid}"
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
-command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/couchpotato.py ${couchpotato_flags}"
-
-# 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
+command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
@@ -59,19 +49,23 @@ fi
verify_couchpotato_pid() {
# Make sure the pid corresponds to the CouchPotato process.
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 $?
}
# Try to stop CouchPotato cleanly by calling shutdown over http.
couchpotato_stop() {
+
echo "Stopping $name"
verify_couchpotato_pid
- ${WGET} -O - -q "http://${HOST}:${PORT}/${CPAPI}/app.shutdown/" >/dev/null
+
if [ -n "${pid}" ]; then
+ kill -SIGTERM ${pid} 2> /dev/null
wait_for_pids ${pid}
+ kill -9 ${pid} 2> /dev/null
echo "Stopped"
fi
+
}
couchpotato_status() {