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() {