Browse Source

Nonblocking update listener

pull/358/merge
Ruud 13 years ago
parent
commit
fcd13fcb85
  1. 32
      couchpotato/api.py
  2. 1
      couchpotato/core/_base/_core/main.py
  3. 85
      couchpotato/core/notifications/core/main.py
  4. 51
      couchpotato/core/notifications/core/static/notification.js
  5. 3
      couchpotato/core/plugins/library/main.py
  6. 1
      couchpotato/core/plugins/movie/main.py
  7. 13
      couchpotato/core/plugins/movie/static/movie.js
  8. 1
      couchpotato/environment.py
  9. 20
      couchpotato/runner.py
  10. 1
      couchpotato/static/scripts/page/wanted.js

32
couchpotato/api.py

@ -1,10 +1,34 @@
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.helpers import url_for from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect from werkzeug.utils import redirect
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
api_docs = {} api_docs = {}
api_docs_missing = [] api_docs_missing = []
api_nonblock = {}
class NonBlockHandler(RequestHandler):
stoppers = []
@asynchronous
def get(self, route):
start, stop = api_nonblock[route]
self.stoppers.append(stop)
start(self.on_new_messages, last_id = self.get_argument("last_id", None))
def on_new_messages(self, response):
if self.request.connection.stream.closed():
return
self.finish(response)
def on_connection_close(self):
for stop in self.stoppers:
stop(self.on_new_messages)
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs) api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
@ -13,6 +37,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
""" Api view """ """ Api view """
def index(): def index():
index_url = url_for('web.index') index_url = url_for('web.index')

1
couchpotato/core/_base/_core/main.py

@ -114,7 +114,6 @@ class Core(Plugin):
log.debug('Save to shutdown/restart') log.debug('Save to shutdown/restart')
try: try:
Env.get('httpserver').stop()
IOLoop.instance().stop() IOLoop.instance().stop()
except RuntimeError: except RuntimeError:
pass pass

85
couchpotato/core/notifications/core/main.py

@ -1,5 +1,5 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading
import time import time
import uuid
log = CPLog(__name__) log = CPLog(__name__)
class CoreNotifier(Notification): class CoreNotifier(Notification):
m_lock = threading.RLock()
messages = [] messages = []
listeners = []
listen_to = [ listen_to = [
'movie.downloaded', 'movie.snatched', 'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
@ -46,8 +51,17 @@ class CoreNotifier(Notification):
}"""} }"""}
}) })
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener) addApiView('notification.listener', self.listener)
def test():
while True:
time.sleep(1)
addEvent('app.load', test)
def markAsRead(self): def markAsRead(self):
ids = [x.strip() for x in getParam('ids').split(',')] ids = [x.strip() for x in getParam('ids').split(',')]
@ -107,25 +121,79 @@ class CoreNotifier(Notification):
ndict = n.to_dict() ndict = n.to_dict()
ndict['type'] = 'notification' ndict['type'] = 'notification'
ndict['time'] = time.time() ndict['time'] = time.time()
self.messages.append(ndict)
self.frontend(type = listener, data = data)
#db.close() #db.close()
return True return True
def frontend(self, type = 'notification', data = {}): def frontend(self, type = 'notification', data = {}):
self.messages.append({
self.m_lock.acquire()
message = {
'id': str(uuid.uuid4()),
'time': time.time(), 'time': time.time(),
'type': type, 'type': type,
'data': data, 'data': data,
}
self.messages.append(message)
while True 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):
for message in self.messages:
if message['time'] < (time.time() - 15):
self.messages.remove(message)
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]["id"] == last_id: break
recent = self.messages[index + 1:]
self.m_lock.release()
return recent or []
def listener(self): def listener(self):
messages = [] messages = []
for message in self.messages:
#delete message older then 15s
if message['time'] > (time.time() - 15):
messages.append(message)
# Get unread # Get unread
if getParam('init'): if getParam('init'):
@ -139,9 +207,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
#db.close()
self.messages = []
return jsonified({ return jsonified({
'success': True, 'success': True,
'result': messages, 'result': messages,

51
couchpotato/core/notifications/core/static/notification.js

@ -8,8 +8,7 @@ var NotificationBase = new Class({
self.setOptions(options); self.setOptions(options);
// Listener // Listener
App.addEvent('load', self.startInterval.bind(self)); App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('unload', self.stopTimer.bind(self));
App.addEvent('notification', self.notify.bind(self)); App.addEvent('notification', self.notify.bind(self));
// Add test buttons to settings page // Add test buttons to settings page
@ -30,7 +29,11 @@ var NotificationBase = new Class({
'href': App.createUrl('notifications'), 'href': App.createUrl('notifications'),
'text': 'Show older notifications' 'text': 'Show older notifications'
})); */ })); */
}) });
window.addEvent('load', function(){
self.startInterval()
});
}, },
@ -86,34 +89,54 @@ var NotificationBase = new Class({
startInterval: function(){ startInterval: function(){
var self = this; var self = this;
self.request = Api.request('notification.listener', { if(self.stopped) return;
'initialDelay': 100,
'delay': 1500, Api.request('notification.listener', {
'data': {'init':true}, 'data': {'init':true},
'onSuccess': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}) }).send()
},
self.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()
startTimer: function(){
if(this.request)
this.request.startTimer()
}, },
stopTimer: function(){ stopPoll: function(){
if(this.request) if(this.request)
this.request.stopTimer() this.request.cancel()
this.stopped = true;
}, },
processData: function(json){ processData: function(json){
var self = this; var self = this;
self.request.options.data = {}
// Process data
if(json){
Array.each(json.result, function(result){ Array.each(json.result, function(result){
App.fireEvent(result.type, result) App.fireEvent(result.type, result)
}) })
self.last_id = json.result.getLast().id
}
// Restart poll
self.startPoll()
}, },
addTestButtons: function(){ addTestButtons: function(){

3
couchpotato/core/plugins/library/main.py

@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
fireEvent('notify.frontend', type = 'library.update.%s' % identifier, data = library_dict)
#db.close()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):

1
couchpotato/core/plugins/movie/main.py

@ -239,6 +239,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
for id in getParam('id').split(','): for id in getParam('id').split(','):
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = id).first()
# Get current selected title # Get current selected title

13
couchpotato/core/plugins/movie/static/movie.js

@ -17,14 +17,16 @@ var Movie = new Class({
self.parent(self, options); self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self)); App.addEvent('movie.update.'+data.id, self.update.bind(self));
App.addEvent('searcher.started.'+data.id, self.searching.bind(self)); App.addEvent('movie.busy.'+data.id, function(notification){
App.addEvent('searcher.ended.'+data.id, self.searching.bind(self)); if(notification.data)
self.busy(true)
});
}, },
searching: function(notification){ busy: function(set_busy){
var self = this; var self = this;
if(notification && notification.type.indexOf('ended') > -1){ if(!set_busy){
if(self.spinner){ if(self.spinner){
self.mask.fade('out'); self.mask.fade('out');
setTimeout(function(){ setTimeout(function(){
@ -72,8 +74,11 @@ var Movie = new Class({
self.data = notification.data; self.data = notification.data;
self.container.destroy(); self.container.destroy();
self.profile = Quality.getProfile(self.data.profile_id) || {}; self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create(); self.create();
self.busy(false);
}, },
create: function(){ create: function(){

1
couchpotato/environment.py

@ -23,7 +23,6 @@ class Env(object):
_deamonize = False _deamonize = False
_desktop = None _desktop = None
_session = None _session = None
_httpserver = None
''' Data paths and directories ''' ''' Data paths and directories '''
_app_dir = "" _app_dir = ""

20
couchpotato/runner.py

@ -1,13 +1,13 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from couchpotato import web from couchpotato import web
from couchpotato.api import api from couchpotato.api import api, NonBlockHandler
from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.event import fireEventAsync, fireEvent
from couchpotato.core.helpers.variable import getDataDir, tryInt from couchpotato.core.helpers.variable import getDataDir, tryInt
from logging import handlers from logging import handlers
from tornado import autoreload from tornado import autoreload
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import RequestHandler from tornado.web import RequestHandler, Application, FallbackHandler
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache from werkzeug.contrib.cache import FileSystemCache
import locale import locale
@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Go go go! # Go go go!
web_container = WSGIContainer(app) web_container = WSGIContainer(app)
web_container._log = _log web_container._log = _log
http_server = HTTPServer(web_container, no_keep_alive = True)
Env.set('httpserver', http_server)
loop = IOLoop.instance() loop = IOLoop.instance()
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
(r'.*', FallbackHandler, dict(fallback = web_container)),
],
log_function = lambda x : None,
debug = config['use_reloader']
)
try_restart = True try_restart = True
restart_tries = 5 restart_tries = 5
while try_restart: while try_restart:
try: try:
http_server.listen(config['port'], config['host']) application.listen(config['port'], config['host'], no_keep_alive = True)
if config['use_reloader']:
autoreload.start(loop)
loop.start() loop.start()
except Exception, e: except Exception, e:
try: try:

1
couchpotato/static/scripts/page/wanted.js

@ -137,7 +137,6 @@ window.addEvent('domready', function(){
var self = this; var self = this;
(e).preventDefault(); (e).preventDefault();
self.movie.searching();
Api.request('movie.refresh', { Api.request('movie.refresh', {
'data': { 'data': {
'id': self.movie.get('id') 'id': self.movie.get('id')

Loading…
Cancel
Save