Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -1,10 +1,39 @@
|
|||||||
from flask.blueprints import Blueprint
|
from flask.blueprints import Blueprint
|
||||||
from flask.helpers import url_for
|
from flask.helpers import url_for
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
from tornado.web import RequestHandler, asynchronous
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
api = Blueprint('api', __name__)
|
||||||
api_docs = {}
|
api_docs = {}
|
||||||
api_docs_missing = []
|
api_docs_missing = []
|
||||||
|
api_nonblock = {}
|
||||||
|
|
||||||
|
|
||||||
|
class NonBlockHandler(RequestHandler):
|
||||||
|
stoppers = []
|
||||||
|
|
||||||
|
@asynchronous
|
||||||
|
def get(self, route):
|
||||||
|
cls = NonBlockHandler
|
||||||
|
start, stop = api_nonblock[route]
|
||||||
|
cls.stoppers.append(stop)
|
||||||
|
|
||||||
|
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
|
||||||
|
|
||||||
|
def onNewMessage(self, response):
|
||||||
|
if self.request.connection.stream.closed():
|
||||||
|
return
|
||||||
|
self.finish(response)
|
||||||
|
|
||||||
|
def on_connection_close(self):
|
||||||
|
cls = NonBlockHandler
|
||||||
|
|
||||||
|
for stop in cls.stoppers:
|
||||||
|
stop(self.onNewMessage)
|
||||||
|
|
||||||
|
cls.stoppers = []
|
||||||
|
|
||||||
|
|
||||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
||||||
@@ -13,6 +42,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
|
|||||||
else:
|
else:
|
||||||
api_docs_missing.append(route)
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
|
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||||
|
api_nonblock[route] = func_tuple
|
||||||
|
|
||||||
|
if docs:
|
||||||
|
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||||
|
else:
|
||||||
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
""" Api view """
|
""" Api view """
|
||||||
def index():
|
def index():
|
||||||
index_url = url_for('web.index')
|
index_url = url_for('web.index')
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class Core(Plugin):
|
|||||||
log.debug('Save to shutdown/restart')
|
log.debug('Save to shutdown/restart')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Env.get('httpserver').stop()
|
|
||||||
IOLoop.instance().stop()
|
IOLoop.instance().stop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class Updater(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.updater = SourceUpdater()
|
self.updater = SourceUpdater()
|
||||||
|
|
||||||
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6)
|
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||||
addEvent('app.load', self.check)
|
addEvent('app.load', self.check)
|
||||||
addEvent('updater.info', self.info)
|
addEvent('updater.info', self.info)
|
||||||
|
|
||||||
@@ -48,17 +48,20 @@ class Updater(Plugin):
|
|||||||
'return': {'type': 'see updater.info'}
|
'return': {'type': 'see updater.info'}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def autoUpdate(self):
|
||||||
|
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||||
|
self.updater.doUpdate()
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
if self.isDisabled():
|
if self.isDisabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.updater.check():
|
if self.updater.check():
|
||||||
if self.conf('automatic') and not self.updater.update_failed:
|
if self.conf('notification') and not self.conf('automatic'):
|
||||||
if self.updater.doUpdate():
|
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
||||||
fireEventAsync('app.restart')
|
return True
|
||||||
else:
|
|
||||||
if self.conf('notification'):
|
return False
|
||||||
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
return self.updater.info()
|
return self.updater.info()
|
||||||
@@ -67,12 +70,22 @@ class Updater(Plugin):
|
|||||||
return jsonified(self.updater.info())
|
return jsonified(self.updater.info())
|
||||||
|
|
||||||
def checkView(self):
|
def checkView(self):
|
||||||
self.check()
|
return jsonified({
|
||||||
return self.updater.getInfo()
|
'update_available': self.check(),
|
||||||
|
'info': self.updater.info()
|
||||||
|
})
|
||||||
|
|
||||||
def doUpdateView(self):
|
def doUpdateView(self):
|
||||||
|
|
||||||
|
self.check()
|
||||||
|
if not self.update_version:
|
||||||
|
log.error('Trying to update when no update is available.')
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
success = self.updater.doUpdate()
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': self.updater.doUpdate()
|
'success': success
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +150,7 @@ class GitUpdater(BaseUpdater):
|
|||||||
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
|
self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
|
||||||
|
|
||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.debug('Stashing local changes')
|
log.debug('Stashing local changes')
|
||||||
self.repo.saveStash()
|
self.repo.saveStash()
|
||||||
@@ -152,6 +166,8 @@ class GitUpdater(BaseUpdater):
|
|||||||
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
||||||
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
||||||
|
|
||||||
|
fireEventAsync('app.restart')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed updating via GIT: %s' % traceback.format_exc())
|
log.error('Failed updating via GIT: %s' % traceback.format_exc())
|
||||||
@@ -243,6 +259,8 @@ class SourceUpdater(BaseUpdater):
|
|||||||
# Write update version to file
|
# Write update version to file
|
||||||
self.createFile(self.version_file, json.dumps(self.update_version))
|
self.createFile(self.version_file, json.dumps(self.update_version))
|
||||||
|
|
||||||
|
fireEventAsync('app.restart')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed updating: %s' % traceback.format_exc())
|
log.error('Failed updating: %s' % traceback.format_exc())
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ var UpdaterBase = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
Api.request('updater.check', {
|
Api.request('updater.check', {
|
||||||
'onComplete': onComplete || Function.from()
|
'onComplete': function(json){
|
||||||
|
if(onComplete)
|
||||||
|
onComplete(json);
|
||||||
|
|
||||||
|
if(json.update_available)
|
||||||
|
self.doUpdate();
|
||||||
|
else
|
||||||
|
App.unBlockPage()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -52,7 +60,7 @@ var UpdaterBase = new Class({
|
|||||||
createMessage: function(data){
|
createMessage: function(data){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.update_version.hash;
|
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
|
||||||
if(data.update_version.changelog)
|
if(data.update_version.changelog)
|
||||||
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
|
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
|
||||||
|
|
||||||
@@ -81,13 +89,19 @@ var UpdaterBase = new Class({
|
|||||||
Api.request('updater.update', {
|
Api.request('updater.update', {
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
if(json.success){
|
if(json.success){
|
||||||
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
self.updating();
|
||||||
App.checkAvailable.delay(500, App);
|
|
||||||
if(self.message)
|
|
||||||
self.message.destroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updating: function(){
|
||||||
|
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||||
|
App.checkAvailable.delay(500, App, [1000, function(){
|
||||||
|
window.location.reload();
|
||||||
|
}]);
|
||||||
|
if(self.message)
|
||||||
|
self.message.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
is_after_event = True
|
is_after_event = True
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
# onComplete event
|
||||||
|
on_complete = False
|
||||||
|
try:
|
||||||
|
on_complete = kwargs['on_complete']
|
||||||
|
del kwargs['on_complete']
|
||||||
|
except: pass
|
||||||
|
|
||||||
# Return single handler
|
# Return single handler
|
||||||
single = False
|
single = False
|
||||||
try:
|
try:
|
||||||
@@ -129,24 +136,23 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
if not is_after_event:
|
if not is_after_event:
|
||||||
fireEvent('%s.after' % name, is_after_event = True)
|
fireEvent('%s.after' % name, is_after_event = True)
|
||||||
|
|
||||||
|
if on_complete:
|
||||||
|
on_complete()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
except KeyError, e:
|
except KeyError, e:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error('%s: %s' % (name, traceback.format_exc()))
|
log.error('%s: %s' % (name, traceback.format_exc()))
|
||||||
|
|
||||||
def fireEventAsync(name, *args, **kwargs):
|
def fireEventAsync(*args, **kwargs):
|
||||||
#log.debug('Async "%s": %s, %s' % (name, args, kwargs))
|
|
||||||
try:
|
try:
|
||||||
e = events[name]
|
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||||
e.lock.acquire()
|
my_thread.setDaemon(True)
|
||||||
e.asynchronous = True
|
my_thread.start()
|
||||||
e.error_handler = errorHandler
|
|
||||||
e(*args, **kwargs)
|
|
||||||
e.lock.release()
|
|
||||||
return True
|
return True
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.error('%s: %s' % (name, e))
|
log.error('%s: %s' % (args[0], e))
|
||||||
|
|
||||||
def errorHandler(error):
|
def errorHandler(error):
|
||||||
etype, value, tb = error
|
etype, value, tb = error
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import natcmp
|
from couchpotato.core.helpers.variable import natcmp
|
||||||
from flask.globals import current_app
|
from flask.globals import current_app
|
||||||
from flask.helpers import json
|
from flask.helpers import json, make_response
|
||||||
from libs.werkzeug.urls import url_decode
|
from libs.werkzeug.urls import url_decode
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
import flask
|
import flask
|
||||||
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
|
|||||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
||||||
|
|
||||||
def jsonified(*args, **kwargs):
|
def jsonified(*args, **kwargs):
|
||||||
from couchpotato.environment import Env
|
|
||||||
callback = getParam('callback_func', None)
|
callback = getParam('callback_func', None)
|
||||||
if callback:
|
if callback:
|
||||||
return padded_jsonify(callback, *args, **kwargs)
|
content = padded_jsonify(callback, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs)
|
content = jsonify('application/json', *args, **kwargs)
|
||||||
|
|
||||||
|
response = make_response(content)
|
||||||
|
response.cache_control.no_cache = True
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView, addNonBlockApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
from couchpotato.core.helpers.request import jsonified, getParam
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
@@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
|
|||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from couchpotato.core.settings.model import Notification as Notif
|
from couchpotato.core.settings.model import Notification as Notif
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CoreNotifier(Notification):
|
class CoreNotifier(Notification):
|
||||||
|
|
||||||
|
m_lock = threading.Lock()
|
||||||
messages = []
|
messages = []
|
||||||
|
listeners = []
|
||||||
|
|
||||||
listen_to = [
|
listen_to = [
|
||||||
'movie.downloaded', 'movie.snatched',
|
'movie.downloaded', 'movie.snatched',
|
||||||
'updater.available', 'updater.updated',
|
'updater.available', 'updater.updated',
|
||||||
@@ -46,14 +51,9 @@ class CoreNotifier(Notification):
|
|||||||
}"""}
|
}"""}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
|
||||||
addApiView('notification.listener', self.listener)
|
addApiView('notification.listener', self.listener)
|
||||||
|
|
||||||
self.registerEvents()
|
|
||||||
|
|
||||||
def registerEvents(self):
|
|
||||||
|
|
||||||
# Library update, frontend refresh
|
|
||||||
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
|
|
||||||
|
|
||||||
def markAsRead(self):
|
def markAsRead(self):
|
||||||
ids = [x.strip() for x in getParam('ids').split(',')]
|
ids = [x.strip() for x in getParam('ids').split(',')]
|
||||||
@@ -114,25 +114,85 @@ class CoreNotifier(Notification):
|
|||||||
ndict = n.to_dict()
|
ndict = n.to_dict()
|
||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
ndict['time'] = time.time()
|
ndict['time'] = time.time()
|
||||||
self.messages.append(ndict)
|
|
||||||
|
self.frontend(type = listener, data = data)
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def frontend(self, type = 'notification', data = {}):
|
def frontend(self, type = 'notification', data = {}):
|
||||||
self.messages.append({
|
|
||||||
|
self.m_lock.acquire()
|
||||||
|
message = {
|
||||||
|
'message_id': str(uuid.uuid4()),
|
||||||
'time': time.time(),
|
'time': time.time(),
|
||||||
'type': type,
|
'type': type,
|
||||||
'data': data,
|
'data': data,
|
||||||
})
|
}
|
||||||
|
self.messages.append(message)
|
||||||
|
|
||||||
|
while len(self.listeners) > 0 and not self.shuttingDown():
|
||||||
|
try:
|
||||||
|
listener, last_id = self.listeners.pop()
|
||||||
|
listener({
|
||||||
|
'success': True,
|
||||||
|
'result': [message],
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.m_lock.release()
|
||||||
|
self.cleanMessages()
|
||||||
|
|
||||||
|
def addListener(self, callback, last_id = None):
|
||||||
|
|
||||||
|
if last_id:
|
||||||
|
messages = self.getMessages(last_id)
|
||||||
|
if len(messages) > 0:
|
||||||
|
return callback({
|
||||||
|
'success': True,
|
||||||
|
'result': messages,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.listeners.append((callback, last_id))
|
||||||
|
|
||||||
|
|
||||||
|
def removeListener(self, callback):
|
||||||
|
|
||||||
|
for list_tuple in self.listeners:
|
||||||
|
try:
|
||||||
|
listener, last_id = list_tuple
|
||||||
|
if listener == callback:
|
||||||
|
self.listeners.remove(list_tuple)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanMessages(self):
|
||||||
|
self.m_lock.acquire()
|
||||||
|
|
||||||
|
for message in self.messages:
|
||||||
|
if message['time'] < (time.time() - 15):
|
||||||
|
self.messages.remove(message)
|
||||||
|
|
||||||
|
self.m_lock.release()
|
||||||
|
|
||||||
|
def getMessages(self, last_id):
|
||||||
|
self.m_lock.acquire()
|
||||||
|
|
||||||
|
recent = []
|
||||||
|
index = 0
|
||||||
|
for i in xrange(len(self.messages)):
|
||||||
|
index = len(self.messages) - i - 1
|
||||||
|
if self.messages[index]["message_id"] == last_id: break
|
||||||
|
recent = self.messages[index:]
|
||||||
|
|
||||||
|
self.m_lock.release()
|
||||||
|
|
||||||
|
return recent or []
|
||||||
|
|
||||||
def listener(self):
|
def listener(self):
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
for message in self.messages:
|
|
||||||
#delete message older then 15s
|
|
||||||
if message['time'] > (time.time() - 15):
|
|
||||||
messages.append(message)
|
|
||||||
|
|
||||||
# Get unread
|
# Get unread
|
||||||
if getParam('init'):
|
if getParam('init'):
|
||||||
@@ -146,9 +206,6 @@ class CoreNotifier(Notification):
|
|||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
messages.append(ndict)
|
messages.append(ndict)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
|
|
||||||
self.messages = []
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
'result': messages,
|
'result': messages,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ var NotificationBase = new Class({
|
|||||||
self.setOptions(options);
|
self.setOptions(options);
|
||||||
|
|
||||||
// Listener
|
// Listener
|
||||||
App.addEvent('load', self.startInterval.bind(self));
|
App.addEvent('unload', self.stopPoll.bind(self));
|
||||||
App.addEvent('unload', self.stopTimer.bind(self));
|
App.addEvent('reload', self.startInterval.bind(self, [true]));
|
||||||
App.addEvent('notification', self.notify.bind(self));
|
App.addEvent('notification', self.notify.bind(self));
|
||||||
|
|
||||||
// Add test buttons to settings page
|
// Add test buttons to settings page
|
||||||
@@ -30,7 +30,11 @@ var NotificationBase = new Class({
|
|||||||
'href': App.createUrl('notifications'),
|
'href': App.createUrl('notifications'),
|
||||||
'text': 'Show older notifications'
|
'text': 'Show older notifications'
|
||||||
})); */
|
})); */
|
||||||
})
|
});
|
||||||
|
|
||||||
|
window.addEvent('load', function(){
|
||||||
|
self.startInterval()
|
||||||
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -83,37 +87,61 @@ var NotificationBase = new Class({
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
startInterval: function(){
|
startInterval: function(force){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.request = Api.request('notification.listener', {
|
if(self.stopped && !force){
|
||||||
'initialDelay': 100,
|
self.stopped = false;
|
||||||
'delay': 3000,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.request('notification.listener', {
|
||||||
'data': {'init':true},
|
'data': {'init':true},
|
||||||
'onSuccess': self.processData.bind(self)
|
'onSuccess': self.processData.bind(self)
|
||||||
})
|
}).send()
|
||||||
|
|
||||||
self.request.startTimer()
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
startTimer: function(){
|
startPoll: function(){
|
||||||
if(this.request)
|
var self = this;
|
||||||
this.request.startTimer()
|
|
||||||
|
if(self.stopped || (self.request && self.request.isRunning()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
self.request = Api.request('nonblock/notification.listener', {
|
||||||
|
'onSuccess': self.processData.bind(self),
|
||||||
|
'data': {
|
||||||
|
'last_id': self.last_id
|
||||||
|
},
|
||||||
|
'onFailure': function(){
|
||||||
|
self.startPoll.delay(2000, self)
|
||||||
|
}
|
||||||
|
}).send()
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stopTimer: function(){
|
stopPoll: function(){
|
||||||
if(this.request)
|
if(this.request)
|
||||||
this.request.stopTimer()
|
this.request.cancel()
|
||||||
|
this.stopped = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
processData: function(json){
|
processData: function(json){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.request.options.data = {}
|
|
||||||
Array.each(json.result, function(result){
|
// Process data
|
||||||
App.fireEvent(result.type, result)
|
if(json){
|
||||||
})
|
Array.each(json.result, function(result){
|
||||||
|
App.fireEvent(result.type, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
if(json.result.length > 0)
|
||||||
|
self.last_id = json.result.getLast().message_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart poll
|
||||||
|
self.startPoll()
|
||||||
},
|
},
|
||||||
|
|
||||||
addTestButtons: function(){
|
addTestButtons: function(){
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from gntp import notifier
|
from gntp import notifier
|
||||||
import logging
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -17,7 +16,7 @@ class Growl(Notification):
|
|||||||
super(Growl, self).__init__()
|
super(Growl, self).__init__()
|
||||||
|
|
||||||
if self.isEnabled():
|
if self.isEnabled():
|
||||||
self.register()
|
addEvent('app.load', self.register)
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
if self.registered: return
|
if self.registered: return
|
||||||
|
|||||||
@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
|
|||||||
|
|
||||||
def view(self):
|
def view(self):
|
||||||
|
|
||||||
|
path = getParam('path', '/')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dirs = self.getDirectories(path = getParam('path', '/'), show_hidden = getParam('show_hidden', True))
|
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||||
except:
|
except:
|
||||||
dirs = []
|
dirs = []
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'is_root': getParam('path', '/') == '/',
|
'is_root': path == '/' or not path,
|
||||||
'empty': len(dirs) == 0,
|
'empty': len(dirs) == 0,
|
||||||
'dirs': dirs,
|
'dirs': dirs,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
library_dict = library.to_dict(self.default_dict)
|
library_dict = library.to_dict(self.default_dict)
|
||||||
|
|
||||||
fireEvent('library.update_finish', data = library_dict)
|
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return library_dict
|
return library_dict
|
||||||
|
|
||||||
def updateReleaseDate(self, identifier):
|
def updateReleaseDate(self, identifier):
|
||||||
@@ -138,8 +135,8 @@ class LibraryPlugin(Plugin):
|
|||||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||||
|
|
||||||
if not library.info:
|
if not library.info:
|
||||||
self.update(identifier)
|
library_dict = self.update(identifier)
|
||||||
dates = library.get('info', {}).get('release_dates')
|
dates = library_dict.get('info', {}).get('release_dates')
|
||||||
else:
|
else:
|
||||||
dates = library.info.get('release_date')
|
dates = library.info.get('release_date')
|
||||||
|
|
||||||
|
|||||||
@@ -239,16 +239,18 @@ class MoviePlugin(Plugin):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
for id in getParam('id').split(','):
|
for id in getParam('id').split(','):
|
||||||
|
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||||
movie = db.query(Movie).filter_by(id = id).first()
|
movie = db.query(Movie).filter_by(id = id).first()
|
||||||
|
|
||||||
# Get current selected title
|
|
||||||
default_title = ''
|
|
||||||
for title in movie.library.titles:
|
|
||||||
if title.default: default_title = title.title
|
|
||||||
|
|
||||||
if movie:
|
if movie:
|
||||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
|
|
||||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
|
# Get current selected title
|
||||||
|
default_title = ''
|
||||||
|
for title in movie.library.titles:
|
||||||
|
if title.default: default_title = title.title
|
||||||
|
|
||||||
|
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||||
|
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return jsonified({
|
return jsonified({
|
||||||
@@ -287,6 +289,7 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
||||||
|
added = True
|
||||||
do_search = False
|
do_search = False
|
||||||
if not m:
|
if not m:
|
||||||
m = Movie(
|
m = Movie(
|
||||||
@@ -295,8 +298,14 @@ class MoviePlugin(Plugin):
|
|||||||
status_id = status_active.get('id'),
|
status_id = status_active.get('id'),
|
||||||
)
|
)
|
||||||
db.add(m)
|
db.add(m)
|
||||||
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
|
db.commit()
|
||||||
do_search = True
|
|
||||||
|
onComplete = None
|
||||||
|
if search_after:
|
||||||
|
onComplete = self.createOnComplete(m.id)
|
||||||
|
|
||||||
|
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||||
|
search_after = False
|
||||||
elif force_readd:
|
elif force_readd:
|
||||||
# Clean snatched history
|
# Clean snatched history
|
||||||
for release in m.releases:
|
for release in m.releases:
|
||||||
@@ -306,9 +315,11 @@ class MoviePlugin(Plugin):
|
|||||||
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
||||||
else:
|
else:
|
||||||
log.debug('Movie already exists, not updating: %s' % params)
|
log.debug('Movie already exists, not updating: %s' % params)
|
||||||
|
added = False
|
||||||
|
|
||||||
if force_readd:
|
if force_readd:
|
||||||
m.status_id = status_active.get('id')
|
m.status_id = status_active.get('id')
|
||||||
|
do_search = True
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -321,8 +332,12 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
movie_dict = m.to_dict(self.default_dict)
|
movie_dict = m.to_dict(self.default_dict)
|
||||||
|
|
||||||
if (force_readd or do_search) and search_after:
|
if do_search and search_after:
|
||||||
fireEventAsync('searcher.single', movie_dict)
|
onComplete = self.createOnComplete(m.id)
|
||||||
|
onComplete()
|
||||||
|
|
||||||
|
if added:
|
||||||
|
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict)
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return movie_dict
|
return movie_dict
|
||||||
@@ -369,7 +384,7 @@ class MoviePlugin(Plugin):
|
|||||||
fireEvent('movie.restatus', m.id)
|
fireEvent('movie.restatus', m.id)
|
||||||
|
|
||||||
movie_dict = m.to_dict(self.default_dict)
|
movie_dict = m.to_dict(self.default_dict)
|
||||||
fireEventAsync('searcher.single', movie_dict)
|
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return jsonified({
|
return jsonified({
|
||||||
@@ -458,3 +473,22 @@ class MoviePlugin(Plugin):
|
|||||||
#db.close()
|
#db.close()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def createOnComplete(self, movie_id):
|
||||||
|
|
||||||
|
def onComplete():
|
||||||
|
db = get_session()
|
||||||
|
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
|
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||||
|
|
||||||
|
return onComplete
|
||||||
|
|
||||||
|
|
||||||
|
def createNotifyFront(self, movie_id):
|
||||||
|
|
||||||
|
def notifyFront():
|
||||||
|
db = get_session()
|
||||||
|
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
|
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||||
|
|
||||||
|
return notifyFront
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ var MovieList = new Class({
|
|||||||
options: {
|
options: {
|
||||||
navigation: true,
|
navigation: true,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
menu: []
|
menu: [],
|
||||||
|
add_new: false
|
||||||
},
|
},
|
||||||
|
|
||||||
movies: [],
|
movies: [],
|
||||||
|
movies_added: {},
|
||||||
letters: {},
|
letters: {},
|
||||||
filter: {
|
filter: {
|
||||||
'startswith': null,
|
'startswith': null,
|
||||||
@@ -30,6 +32,17 @@ var MovieList = new Class({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
self.getMovies();
|
self.getMovies();
|
||||||
|
|
||||||
|
if(options.add_new)
|
||||||
|
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||||
|
},
|
||||||
|
|
||||||
|
movieAdded: function(notification){
|
||||||
|
var self = this;
|
||||||
|
window.scroll(0,0);
|
||||||
|
|
||||||
|
if(!self.movies_added[notification.data.id])
|
||||||
|
self.createMovie(notification.data, 'top');
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
@@ -71,26 +84,31 @@ var MovieList = new Class({
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.each(movies, function(movie){
|
Object.each(movies, function(movie){
|
||||||
|
self.createMovie(movie);
|
||||||
// Attach proper actions
|
|
||||||
var a = self.options.actions,
|
|
||||||
status = Status.get(movie.status_id);
|
|
||||||
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
|
|
||||||
|
|
||||||
var m = new Movie(self, {
|
|
||||||
'actions': actions,
|
|
||||||
'view': self.current_view,
|
|
||||||
'onSelect': self.calculateSelected.bind(self)
|
|
||||||
}, movie);
|
|
||||||
$(m).inject(self.movie_list);
|
|
||||||
m.fireEvent('injected');
|
|
||||||
|
|
||||||
self.movies.include(m)
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createMovie: function(movie, inject_at){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Attach proper actions
|
||||||
|
var a = self.options.actions,
|
||||||
|
status = Status.get(movie.status_id);
|
||||||
|
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
|
||||||
|
|
||||||
|
var m = new Movie(self, {
|
||||||
|
'actions': actions,
|
||||||
|
'view': self.current_view,
|
||||||
|
'onSelect': self.calculateSelected.bind(self)
|
||||||
|
}, movie);
|
||||||
|
$(m).inject(self.movie_list, inject_at || 'bottom');
|
||||||
|
m.fireEvent('injected');
|
||||||
|
|
||||||
|
self.movies.include(m)
|
||||||
|
self.movies_added[movie.id] = true;
|
||||||
|
},
|
||||||
|
|
||||||
createNavigation: function(){
|
createNavigation: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
@@ -260,6 +278,7 @@ var MovieList = new Class({
|
|||||||
'events': {
|
'events': {
|
||||||
'click': function(e){
|
'click': function(e){
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
this.set('text', 'Deleting..')
|
||||||
Api.request('movie.delete', {
|
Api.request('movie.delete', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': ids.join(','),
|
'id': ids.join(','),
|
||||||
@@ -268,14 +287,19 @@ var MovieList = new Class({
|
|||||||
'onSuccess': function(){
|
'onSuccess': function(){
|
||||||
qObj.close();
|
qObj.close();
|
||||||
|
|
||||||
|
var erase_movies = [];
|
||||||
self.movies.each(function(movie){
|
self.movies.each(function(movie){
|
||||||
if (movie.isSelected()){
|
if (movie.isSelected()){
|
||||||
$(movie).destroy()
|
$(movie).destroy()
|
||||||
self.movies.erase(movie)
|
erase_movies.include(movie)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.calculateSelected()
|
erase_movies.each(function(movie){
|
||||||
|
self.movies.erase(movie);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.calculateSelected();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
width: 64%;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .info .year {
|
.movies .info .year {
|
||||||
@@ -152,7 +152,6 @@
|
|||||||
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
|
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
float: right;
|
float: right;
|
||||||
width: 30%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .data .quality .available, .movies .data .quality .snatched {
|
.movies .data .quality .available, .movies .data .quality .snatched {
|
||||||
@@ -200,6 +199,7 @@
|
|||||||
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
|
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
|
||||||
margin: -34px 2px 0 0;
|
margin: -34px 2px 0 0;
|
||||||
background: #4e5969;
|
background: #4e5969;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .delete_container {
|
.movies .delete_container {
|
||||||
|
|||||||
@@ -11,53 +11,121 @@ var Movie = new Class({
|
|||||||
self.view = options.view || 'thumbs';
|
self.view = options.view || 'thumbs';
|
||||||
self.list = list;
|
self.list = list;
|
||||||
|
|
||||||
|
self.el = new Element('div.movie.inlay');
|
||||||
|
|
||||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||||
self.parent(self, options);
|
self.parent(self, options);
|
||||||
|
|
||||||
|
App.addEvent('movie.update.'+data.id, self.update.bind(self));
|
||||||
|
App.addEvent('movie.busy.'+data.id, function(notification){
|
||||||
|
if(notification.data)
|
||||||
|
self.busy(true)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
busy: function(set_busy){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!set_busy){
|
||||||
|
if(self.spinner){
|
||||||
|
self.mask.fade('out');
|
||||||
|
setTimeout(function(){
|
||||||
|
if(self.mask)
|
||||||
|
self.mask.destroy();
|
||||||
|
if(self.spinner)
|
||||||
|
self.spinner.el.destroy();
|
||||||
|
self.spinner = null;
|
||||||
|
self.mask = null;
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(!self.spinner) {
|
||||||
|
self.createMask();
|
||||||
|
self.spinner = createSpinner(self.mask);
|
||||||
|
self.positionMask();
|
||||||
|
self.mask.fade('in');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createMask: function(){
|
||||||
|
var self = this;
|
||||||
|
self.mask = new Element('div.mask', {
|
||||||
|
'styles': {
|
||||||
|
'z-index': '1'
|
||||||
|
}
|
||||||
|
}).inject(self.el, 'top').fade('hide');
|
||||||
|
self.positionMask();
|
||||||
|
},
|
||||||
|
|
||||||
|
positionMask: function(){
|
||||||
|
var self = this,
|
||||||
|
s = self.el.getSize()
|
||||||
|
|
||||||
|
return self.mask.setStyles({
|
||||||
|
'width': s.x,
|
||||||
|
'height': s.y
|
||||||
|
}).position({
|
||||||
|
'relativeTo': self.el
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(notification){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.data = notification.data;
|
||||||
|
self.container.destroy();
|
||||||
|
|
||||||
|
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||||
|
self.create();
|
||||||
|
|
||||||
|
self.busy(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('div.movie.inlay').adopt(
|
self.el.adopt(
|
||||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
self.container = new Element('div.movie_container').adopt(
|
||||||
'events': {
|
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||||
'change': function(){
|
'events': {
|
||||||
self.fireEvent('select')
|
'change': function(){
|
||||||
}
|
self.fireEvent('select')
|
||||||
}
|
|
||||||
}),
|
|
||||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
|
||||||
self.data_container = new Element('div.data.inlay.light', {
|
|
||||||
'tween': {
|
|
||||||
duration: 400,
|
|
||||||
transition: 'quint:in:out',
|
|
||||||
onComplete: self.fireEvent.bind(self, 'slideEnd')
|
|
||||||
}
|
|
||||||
}).adopt(
|
|
||||||
self.info_container = new Element('div.info').adopt(
|
|
||||||
self.title = new Element('div.title', {
|
|
||||||
'text': self.getTitle() || 'n/a'
|
|
||||||
}),
|
|
||||||
self.year = new Element('div.year', {
|
|
||||||
'text': self.data.library.year || 'n/a'
|
|
||||||
}),
|
|
||||||
self.rating = new Element('div.rating.icon', {
|
|
||||||
'text': self.data.library.rating
|
|
||||||
}),
|
|
||||||
self.description = new Element('div.description', {
|
|
||||||
'text': self.data.library.plot
|
|
||||||
}),
|
|
||||||
self.quality = new Element('div.quality', {
|
|
||||||
'events': {
|
|
||||||
'click': function(e){
|
|
||||||
var releases = self.el.getElement('.actions .releases');
|
|
||||||
if(releases)
|
|
||||||
releases.fireEvent('click', [e])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
),
|
}),
|
||||||
self.actions = new Element('div.actions')
|
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||||
|
self.data_container = new Element('div.data.inlay.light', {
|
||||||
|
'tween': {
|
||||||
|
duration: 400,
|
||||||
|
transition: 'quint:in:out',
|
||||||
|
onComplete: self.fireEvent.bind(self, 'slideEnd')
|
||||||
|
}
|
||||||
|
}).adopt(
|
||||||
|
self.info_container = new Element('div.info').adopt(
|
||||||
|
self.title = new Element('div.title', {
|
||||||
|
'text': self.getTitle() || 'n/a'
|
||||||
|
}),
|
||||||
|
self.year = new Element('div.year', {
|
||||||
|
'text': self.data.library.year || 'n/a'
|
||||||
|
}),
|
||||||
|
self.rating = new Element('div.rating.icon', {
|
||||||
|
'text': self.data.library.rating
|
||||||
|
}),
|
||||||
|
self.description = new Element('div.description', {
|
||||||
|
'text': self.data.library.plot
|
||||||
|
}),
|
||||||
|
self.quality = new Element('div.quality', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
var releases = self.el.getElement('.actions .releases');
|
||||||
|
if(releases)
|
||||||
|
releases.fireEvent('click', [e])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
self.actions = new Element('div.actions')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,7 +218,7 @@ var Movie = new Class({
|
|||||||
self.el.removeEvents('outerClick')
|
self.el.removeEvents('outerClick')
|
||||||
|
|
||||||
self.addEvent('slideEnd:once', function(){
|
self.addEvent('slideEnd:once', function(){
|
||||||
self.el.getElements('> :not(.data):not(.poster)').hide();
|
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.data_container.tween('right', -840, 0);
|
self.data_container.tween('right', -840, 0);
|
||||||
|
|||||||
@@ -221,7 +221,9 @@ Block.Search.Item = new Class({
|
|||||||
}
|
}
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||||
'src': info.images.poster[0]
|
'src': info.images.poster[0],
|
||||||
|
'height': null,
|
||||||
|
'width': null
|
||||||
}) : null,
|
}) : null,
|
||||||
new Element('div.info').adopt(
|
new Element('div.info').adopt(
|
||||||
self.title = new Element('h2', {
|
self.title = new Element('h2', {
|
||||||
@@ -332,8 +334,10 @@ Block.Search.Item = new Class({
|
|||||||
|
|
||||||
self.options.adopt(
|
self.options.adopt(
|
||||||
new Element('div').adopt(
|
new Element('div').adopt(
|
||||||
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||||
'src': self.info.images.poster[0]
|
'src': self.info.images.poster[0],
|
||||||
|
'height': null,
|
||||||
|
'width': null
|
||||||
}) : null,
|
}) : null,
|
||||||
self.info.in_wanted ? new Element('span.in_wanted', {
|
self.info.in_wanted ? new Element('span.in_wanted', {
|
||||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ rename_options = {
|
|||||||
'audio': 'Audio (DTS)',
|
'audio': 'Audio (DTS)',
|
||||||
'group': 'Releasegroup name',
|
'group': 'Releasegroup name',
|
||||||
'source': 'Source media (Bluray)',
|
'source': 'Source media (Bluray)',
|
||||||
'filename': 'Original filename',
|
'original': 'Original filename',
|
||||||
'original_folder': 'Original foldername',
|
'original_folder': 'Original foldername',
|
||||||
'imdb_id': 'IMDB id (tt0123456)',
|
'imdb_id': 'IMDB id (tt0123456)',
|
||||||
|
'cd': 'CD number (cd1)',
|
||||||
|
'cd_nr': 'Just the cd nr. (1)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class Renamer(Plugin):
|
|||||||
if file_type is 'subtitle':
|
if file_type is 'subtitle':
|
||||||
|
|
||||||
# rename subtitles with or without language
|
# rename subtitles with or without language
|
||||||
#rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||||
sub_langs = group['subtitle_language'].get(current_file, [])
|
sub_langs = group['subtitle_language'].get(current_file, [])
|
||||||
|
|
||||||
rename_extras = self.getRenameExtras(
|
rename_extras = self.getRenameExtras(
|
||||||
@@ -314,7 +314,6 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
break
|
break
|
||||||
elif release.status_id is snatched_status.get('id'):
|
elif release.status_id is snatched_status.get('id'):
|
||||||
print release.quality.label, group['meta_data']['quality']['label']
|
|
||||||
if release.quality.id is group['meta_data']['quality']['id']:
|
if release.quality.id is group['meta_data']['quality']['id']:
|
||||||
log.debug('Marking release as downloaded')
|
log.debug('Marking release as downloaded')
|
||||||
release.status_id = downloaded_status.get('id')
|
release.status_id = downloaded_status.get('id')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
|||||||
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
|
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import File
|
from couchpotato.core.settings.model import File, Movie
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from enzyme.exceptions import NoParserError, ParseError
|
from enzyme.exceptions import NoParserError, ParseError
|
||||||
from guessit import guess_movie_info
|
from guessit import guess_movie_info
|
||||||
@@ -27,7 +27,7 @@ class Scanner(Plugin):
|
|||||||
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
||||||
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
||||||
extensions = {
|
extensions = {
|
||||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'],
|
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
||||||
'movie_extra': ['mds'],
|
'movie_extra': ['mds'],
|
||||||
'dvd': ['vts_*', 'vob'],
|
'dvd': ['vts_*', 'vob'],
|
||||||
'nfo': ['nfo', 'txt', 'tag'],
|
'nfo': ['nfo', 'txt', 'tag'],
|
||||||
@@ -161,6 +161,8 @@ class Scanner(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
|
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
for file_path in files:
|
for file_path in files:
|
||||||
|
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
@@ -237,19 +239,51 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
|
|
||||||
# Group the files based on the identifier
|
# Group the files based on the identifier
|
||||||
for identifier, group in movie_files.iteritems():
|
delete_identifiers = []
|
||||||
|
for identifier, found_files in self.path_identifiers.iteritems():
|
||||||
log.debug('Grouping files on identifier: %s' % identifier)
|
log.debug('Grouping files on identifier: %s' % identifier)
|
||||||
|
|
||||||
found_files = set(self.path_identifiers.get(identifier, []))
|
group = movie_files.get(identifier)
|
||||||
group['unsorted_files'].extend(found_files)
|
if group:
|
||||||
|
group['unsorted_files'].extend(found_files)
|
||||||
|
delete_identifiers.append(identifier)
|
||||||
|
|
||||||
# Remove the found files from the leftover stack
|
# Remove the found files from the leftover stack
|
||||||
leftovers = leftovers - found_files
|
leftovers = leftovers - set(found_files)
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Cleaning up used
|
||||||
|
for identifier in delete_identifiers:
|
||||||
|
del self.path_identifiers[identifier]
|
||||||
|
del delete_identifiers
|
||||||
|
|
||||||
|
# Group based on folder
|
||||||
|
delete_identifiers = []
|
||||||
|
for identifier, found_files in self.path_identifiers.iteritems():
|
||||||
|
log.debug('Grouping files on foldername: %s' % identifier)
|
||||||
|
|
||||||
|
for ff in found_files:
|
||||||
|
new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
|
||||||
|
|
||||||
|
group = movie_files.get(new_identifier)
|
||||||
|
if group:
|
||||||
|
group['unsorted_files'].extend([ff])
|
||||||
|
delete_identifiers.append(identifier)
|
||||||
|
|
||||||
|
# Remove the found files from the leftover stack
|
||||||
|
leftovers = leftovers - set([ff])
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Cleaning up used
|
||||||
|
for identifier in delete_identifiers:
|
||||||
|
del self.path_identifiers[identifier]
|
||||||
|
del delete_identifiers
|
||||||
|
|
||||||
# Determine file types
|
# Determine file types
|
||||||
processed_movies = {}
|
processed_movies = {}
|
||||||
@@ -327,6 +361,10 @@ class Scanner(Plugin):
|
|||||||
group['library'] = self.determineMovie(group)
|
group['library'] = self.determineMovie(group)
|
||||||
if not group['library']:
|
if not group['library']:
|
||||||
log.error('Unable to determine movie: %s' % group['identifiers'])
|
log.error('Unable to determine movie: %s' % group['identifiers'])
|
||||||
|
else:
|
||||||
|
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
||||||
|
group['movie_id'] = None if not movie else movie.id
|
||||||
|
|
||||||
|
|
||||||
processed_movies[identifier] = group
|
processed_movies[identifier] = group
|
||||||
|
|
||||||
@@ -647,7 +685,7 @@ class Scanner(Plugin):
|
|||||||
identifier = self.removeCPTag(identifier)
|
identifier = self.removeCPTag(identifier)
|
||||||
|
|
||||||
# groups, release tags, scenename cleaner, regex isn't correct
|
# groups, release tags, scenename cleaner, regex isn't correct
|
||||||
identifier = re.sub(self.clean, '::', simplifyString(identifier))
|
identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
|
||||||
|
|
||||||
# Year
|
# Year
|
||||||
year = self.findYear(identifier)
|
year = self.findYear(identifier)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'ignored_words',
|
'name': 'ignored_words',
|
||||||
'label': 'Ignored words',
|
'label': 'Ignored words',
|
||||||
'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
|
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ class Searcher(Plugin):
|
|||||||
if not default_title:
|
if not default_title:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True)
|
||||||
|
|
||||||
|
ret = False
|
||||||
for quality_type in movie['profile']['types']:
|
for quality_type in movie['profile']['types']:
|
||||||
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
||||||
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
|
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
|
||||||
@@ -107,7 +110,7 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
# Check if movie isn't deleted while searching
|
# Check if movie isn't deleted while searching
|
||||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
||||||
return
|
break
|
||||||
|
|
||||||
# Add them to this movie releases list
|
# Add them to this movie releases list
|
||||||
for nzb in sorted_results:
|
for nzb in sorted_results:
|
||||||
@@ -144,7 +147,8 @@ class Searcher(Plugin):
|
|||||||
for nzb in sorted_results:
|
for nzb in sorted_results:
|
||||||
downloaded = self.download(data = nzb, movie = movie)
|
downloaded = self.download(data = nzb, movie = movie)
|
||||||
if downloaded is True:
|
if downloaded is True:
|
||||||
return True
|
ret = True
|
||||||
|
break
|
||||||
elif downloaded != 'try_next':
|
elif downloaded != 'try_next':
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -153,11 +157,13 @@ class Searcher(Plugin):
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown() or ret:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return False
|
return ret
|
||||||
|
|
||||||
def download(self, data, movie, manual = False):
|
def download(self, data, movie, manual = False):
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.variable import getExt
|
from couchpotato.core.helpers.variable import getExt, getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
import os
|
import os
|
||||||
@@ -17,6 +17,9 @@ class Trailer(Plugin):
|
|||||||
if self.isDisabled() or len(group['files']['trailer']) > 0: return
|
if self.isDisabled() or len(group['files']['trailer']) > 0: return
|
||||||
|
|
||||||
trailers = fireEvent('trailer.search', group = group, merge = True)
|
trailers = fireEvent('trailer.search', group = group, merge = True)
|
||||||
|
if not trailers or trailers == []:
|
||||||
|
log.info('No trailers found for: %s' % getTitle(group['library']))
|
||||||
|
return
|
||||||
|
|
||||||
for trailer in trailers.get(self.conf('quality'), []):
|
for trailer in trailers.get(self.conf('quality'), []):
|
||||||
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
|
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ config = [{
|
|||||||
'tab': 'automation',
|
'tab': 'automation',
|
||||||
'name': 'imdb_automation',
|
'name': 'imdb_automation',
|
||||||
'label': 'IMDB',
|
'label': 'IMDB',
|
||||||
'description': 'From any <strong>public</strong> IMDB watchlists',
|
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'automation_enabled',
|
'name': 'automation_enabled',
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from couchpotato.core.helpers.variable import md5
|
from couchpotato.core.helpers.rss import RSS
|
||||||
|
from couchpotato.core.helpers.variable import md5, getImdb
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.automation.base import Automation
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
import StringIO
|
|
||||||
import csv
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import xml.etree.ElementTree as XMLTree
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IMDB(Automation):
|
class IMDB(Automation, RSS):
|
||||||
|
|
||||||
interval = 1800
|
interval = 1800
|
||||||
|
|
||||||
@@ -21,34 +21,41 @@ class IMDB(Automation):
|
|||||||
return
|
return
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
headers = {}
|
|
||||||
|
|
||||||
for csv_url in self.conf('automation_urls').split(','):
|
enablers = self.conf('automation_urls_use').split(',')
|
||||||
prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
|
|
||||||
|
index = -1
|
||||||
|
for rss_url in self.conf('automation_urls').split(','):
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
if not enablers[index]:
|
||||||
|
continue
|
||||||
|
elif 'rss.imdb' not in rss_url:
|
||||||
|
log.error('This isn\'t the correct url.: %s' % rss_url)
|
||||||
|
continue
|
||||||
|
|
||||||
|
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
|
||||||
last_update = float(Env.prop(prop_name, default = 0))
|
last_update = float(Env.prop(prop_name, default = 0))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cache_key = 'imdb_csv.%s' % md5(csv_url)
|
cache_key = 'imdb.rss.%s' % md5(rss_url)
|
||||||
csv_data = self.getCache(cache_key, csv_url)
|
|
||||||
csv_reader = csv.reader(StringIO.StringIO(csv_data))
|
|
||||||
if not headers:
|
|
||||||
nr = 0
|
|
||||||
for column in csv_reader.next():
|
|
||||||
headers[column] = nr
|
|
||||||
nr += 1
|
|
||||||
|
|
||||||
for row in csv_reader:
|
rss_data = self.getCache(cache_key, rss_url)
|
||||||
created = int(time.mktime(parse(row[headers['created']]).timetuple()))
|
data = XMLTree.fromstring(rss_data)
|
||||||
if created < last_update:
|
rss_movies = self.getElements(data, 'channel/item')
|
||||||
|
|
||||||
|
for movie in rss_movies:
|
||||||
|
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
|
||||||
|
imdb = getImdb(self.getTextElement(movie, "link"))
|
||||||
|
|
||||||
|
if not imdb or created < last_update:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
imdb = row[headers['const']]
|
movies.append(imdb)
|
||||||
if imdb:
|
|
||||||
movies.append(imdb)
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc()))
|
log.error('Failed loading IMDB watchlist: %s %s' % (rss_url, traceback.format_exc()))
|
||||||
|
|
||||||
Env.prop(prop_name, time.time())
|
Env.prop(prop_name, time.time())
|
||||||
|
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class TheMovieDb(MovieProvider):
|
|||||||
|
|
||||||
movie_data = {
|
movie_data = {
|
||||||
'via_tmdb': True,
|
'via_tmdb': True,
|
||||||
'id': int(movie.get('id', 0)),
|
'tmdb_id': int(movie.get('id', 0)),
|
||||||
'titles': [toUnicode(movie.get('name'))],
|
'titles': [toUnicode(movie.get('name'))],
|
||||||
'original_title': movie.get('original_name'),
|
'original_title': movie.get('original_name'),
|
||||||
'images': {
|
'images': {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class Mysterbin(NZBProvider):
|
|||||||
'size': size,
|
'size': size,
|
||||||
'url': self.urls['download'] % myster_id,
|
'url': self.urls['download'] % myster_id,
|
||||||
'description': description,
|
'description': description,
|
||||||
|
'download': self.download,
|
||||||
'check_nzb': False,
|
'check_nzb': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class Newzbin(NZBProvider, RSS):
|
|||||||
|
|
||||||
url = "%s?%s" % (self.urls['search'], arguments)
|
url = "%s?%s" % (self.urls['search'], arguments)
|
||||||
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
|
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
|
||||||
single_cat = True
|
|
||||||
|
|
||||||
data = self.getCache(cache_key)
|
data = self.getCache(cache_key)
|
||||||
if not data:
|
if not data:
|
||||||
@@ -118,7 +117,7 @@ class Newzbin(NZBProvider, RSS):
|
|||||||
|
|
||||||
is_correct_movie = fireEvent('searcher.correct_movie',
|
is_correct_movie = fireEvent('searcher.correct_movie',
|
||||||
nzb = new, movie = movie, quality = quality,
|
nzb = new, movie = movie, quality = quality,
|
||||||
imdb_results = True, single_category = single_cat, single = True)
|
imdb_results = True, single = True)
|
||||||
if is_correct_movie:
|
if is_correct_movie:
|
||||||
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
new['score'] = fireEvent('score.calculate', new, movie, single = True)
|
||||||
results.append(new)
|
results.append(new)
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class NzbIndex(NZBProvider, RSS):
|
|||||||
'id': nzbindex_id,
|
'id': nzbindex_id,
|
||||||
'type': 'nzb',
|
'type': 'nzb',
|
||||||
'provider': self.getName(),
|
'provider': self.getName(),
|
||||||
|
'download': self.download,
|
||||||
'name': self.getTextElement(nzb, "title"),
|
'name': self.getTextElement(nzb, "title"),
|
||||||
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||||
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class Env(object):
|
|||||||
_deamonize = False
|
_deamonize = False
|
||||||
_desktop = None
|
_desktop = None
|
||||||
_session = None
|
_session = None
|
||||||
_httpserver = None
|
|
||||||
|
|
||||||
''' Data paths and directories '''
|
''' Data paths and directories '''
|
||||||
_app_dir = ""
|
_app_dir = ""
|
||||||
|
|||||||
+11
-9
@@ -1,13 +1,13 @@
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from couchpotato import web
|
from couchpotato import web
|
||||||
from couchpotato.api import api
|
from couchpotato.api import api, NonBlockHandler
|
||||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
from tornado import autoreload
|
from tornado import autoreload
|
||||||
from tornado.httpserver import HTTPServer
|
from tornado.httpserver import HTTPServer
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
from tornado.web import RequestHandler
|
from tornado.web import RequestHandler, Application, FallbackHandler
|
||||||
from tornado.wsgi import WSGIContainer
|
from tornado.wsgi import WSGIContainer
|
||||||
from werkzeug.contrib.cache import FileSystemCache
|
from werkzeug.contrib.cache import FileSystemCache
|
||||||
import locale
|
import locale
|
||||||
@@ -227,20 +227,22 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
# Go go go!
|
# Go go go!
|
||||||
web_container = WSGIContainer(app)
|
web_container = WSGIContainer(app)
|
||||||
web_container._log = _log
|
web_container._log = _log
|
||||||
http_server = HTTPServer(web_container, no_keep_alive = True)
|
|
||||||
Env.set('httpserver', http_server)
|
|
||||||
loop = IOLoop.instance()
|
loop = IOLoop.instance()
|
||||||
|
|
||||||
|
application = Application([
|
||||||
|
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
|
||||||
|
(r'.*', FallbackHandler, dict(fallback = web_container)),
|
||||||
|
],
|
||||||
|
log_function = lambda x : None,
|
||||||
|
debug = config['use_reloader']
|
||||||
|
)
|
||||||
|
|
||||||
try_restart = True
|
try_restart = True
|
||||||
restart_tries = 5
|
restart_tries = 5
|
||||||
|
|
||||||
while try_restart:
|
while try_restart:
|
||||||
try:
|
try:
|
||||||
http_server.listen(config['port'], config['host'])
|
application.listen(config['port'], config['host'], no_keep_alive = True)
|
||||||
|
|
||||||
if config['use_reloader']:
|
|
||||||
autoreload.start(loop)
|
|
||||||
|
|
||||||
loop.start()
|
loop.start()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -24,8 +24,8 @@ var CouchPotato = new Class({
|
|||||||
|
|
||||||
if(window.location.hash)
|
if(window.location.hash)
|
||||||
History.handleInitialState();
|
History.handleInitialState();
|
||||||
else
|
|
||||||
self.openPage(window.location.pathname);
|
self.openPage(window.location.pathname);
|
||||||
|
|
||||||
History.addEvent('change', self.openPage.bind(self));
|
History.addEvent('change', self.openPage.bind(self));
|
||||||
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
|
self.c.addEvent('click:relay(a[href^=/]:not([target]))', self.pushState.bind(self));
|
||||||
@@ -211,27 +211,30 @@ var CouchPotato = new Class({
|
|||||||
}]);
|
}]);
|
||||||
},
|
},
|
||||||
|
|
||||||
checkForUpdate: function(func){
|
checkForUpdate: function(onComplete){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
Updater.check(func)
|
Updater.check(onComplete)
|
||||||
|
|
||||||
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
|
self.blockPage('Please wait. If this takes to long, something must have gone wrong.', 'Checking for updates');
|
||||||
self.checkAvailable(3000);
|
self.checkAvailable(3000);
|
||||||
},
|
},
|
||||||
|
|
||||||
checkAvailable: function(delay){
|
checkAvailable: function(delay, onAvailable){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
|
|
||||||
Api.request('app.available', {
|
Api.request('app.available', {
|
||||||
'onFailure': function(){
|
'onFailure': function(){
|
||||||
self.checkAvailable.delay(1000, self);
|
self.checkAvailable.delay(1000, self, [delay, onAvailable]);
|
||||||
self.fireEvent('unload');
|
self.fireEvent('unload');
|
||||||
},
|
},
|
||||||
'onSuccess': function(){
|
'onSuccess': function(){
|
||||||
|
if(onAvailable)
|
||||||
|
onAvailable()
|
||||||
self.unBlockPage();
|
self.unBlockPage();
|
||||||
|
self.fireEvent('reload');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -241,6 +244,8 @@ var CouchPotato = new Class({
|
|||||||
blockPage: function(message, title){
|
blockPage: function(message, title){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
self.unBlockPage();
|
||||||
|
|
||||||
var body = $(document.body);
|
var body = $(document.body);
|
||||||
self.mask = new Element('div.mask').adopt(
|
self.mask = new Element('div.mask').adopt(
|
||||||
new Element('div').adopt(
|
new Element('div').adopt(
|
||||||
@@ -256,20 +261,14 @@ var CouchPotato = new Class({
|
|||||||
|
|
||||||
unBlockPage: function(){
|
unBlockPage: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
self.mask.get('tween').start('opacity', 0).chain(function(){
|
if(self.mask)
|
||||||
this.element.destroy()
|
self.mask.get('tween').start('opacity', 0).chain(function(){
|
||||||
});
|
this.element.destroy()
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createUrl: function(action, params){
|
createUrl: function(action, params){
|
||||||
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
|
return this.options.base_url + (action ? action+'/' : '') + (params ? '?'+Object.toQueryString(params) : '')
|
||||||
},
|
|
||||||
|
|
||||||
notify: function(options){
|
|
||||||
return this.growl.notify({
|
|
||||||
title: "this scrolls away",
|
|
||||||
text: "test - hello there. mouseover to pause away action"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ var AboutSettingTab = new Class({
|
|||||||
'text': 'Getting version...',
|
'text': 'Getting version...',
|
||||||
'events': {
|
'events': {
|
||||||
'click': App.checkForUpdate.bind(App, function(json){
|
'click': App.checkForUpdate.bind(App, function(json){
|
||||||
self.fillVersion(json)
|
self.fillVersion(json.info)
|
||||||
}),
|
}),
|
||||||
'mouseenter': function(){
|
'mouseenter': function(){
|
||||||
this.set('text', 'Check for updates')
|
this.set('text', 'Check for updates')
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ Option.Directory = new Class({
|
|||||||
var v = self.input.get('text');
|
var v = self.input.get('text');
|
||||||
var previous_dir = self.getParentDir();
|
var previous_dir = self.getParentDir();
|
||||||
|
|
||||||
if(previous_dir != v && previous_dir.length > 1){
|
if(previous_dir != v && previous_dir.length >= 1 && !json.is_root){
|
||||||
self.back_button.set('data-value', previous_dir)
|
self.back_button.set('data-value', previous_dir)
|
||||||
self.back_button.set('html', '« '+self.getCurrentDirname(previous_dir))
|
self.back_button.set('html', '« '+self.getCurrentDirname(previous_dir))
|
||||||
self.back_button.show()
|
self.back_button.show()
|
||||||
@@ -909,6 +909,7 @@ Option.Choice = new Class({
|
|||||||
var input = self.tag_input.getElement('li:last-child input');
|
var input = self.tag_input.getElement('li:last-child input');
|
||||||
input.fireEvent('focus');
|
input.fireEvent('focus');
|
||||||
input.focus();
|
input.focus();
|
||||||
|
input.setCaretPosition(input.get('value').length);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.el.addEvent('outerClick', function(){
|
self.el.addEvent('outerClick', function(){
|
||||||
@@ -965,6 +966,12 @@ Option.Choice = new Class({
|
|||||||
'onChange': self.setOrder.bind(self),
|
'onChange': self.setOrder.bind(self),
|
||||||
'onBlur': function(){
|
'onBlur': function(){
|
||||||
self.addLastTag();
|
self.addLastTag();
|
||||||
|
},
|
||||||
|
'onGoLeft': function(){
|
||||||
|
self.goLeft(this)
|
||||||
|
},
|
||||||
|
'onGoRight': function(){
|
||||||
|
self.goRight(this)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$(tag).inject(self.tag_input);
|
$(tag).inject(self.tag_input);
|
||||||
@@ -979,6 +986,30 @@ Option.Choice = new Class({
|
|||||||
return tag;
|
return tag;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
goLeft: function(from_tag){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
from_tag.blur();
|
||||||
|
|
||||||
|
var prev_index = self.tags.indexOf(from_tag)-1;
|
||||||
|
if(prev_index >= 0)
|
||||||
|
self.tags[prev_index].selectFrom('right')
|
||||||
|
else
|
||||||
|
from_tag.focus();
|
||||||
|
|
||||||
|
},
|
||||||
|
goRight: function(from_tag){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
from_tag.blur();
|
||||||
|
|
||||||
|
var next_index = self.tags.indexOf(from_tag)+1;
|
||||||
|
if(next_index < self.tags.length)
|
||||||
|
self.tags[next_index].selectFrom('left')
|
||||||
|
else
|
||||||
|
from_tag.focus();
|
||||||
|
},
|
||||||
|
|
||||||
setOrder: function(){
|
setOrder: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -1059,7 +1090,16 @@ Option.Choice.Tag = new Class({
|
|||||||
'width': 0
|
'width': 0
|
||||||
},
|
},
|
||||||
'events': {
|
'events': {
|
||||||
'keyup': self.is_choice ? null : function(){
|
'keyup': self.is_choice ? null : function(e){
|
||||||
|
var current_caret_pos = self.input.getCaretPosition();
|
||||||
|
if(e.key == 'left' && current_caret_pos == self.last_caret_pos){
|
||||||
|
self.fireEvent('goLeft');
|
||||||
|
}
|
||||||
|
else if (e.key == 'right' && self.last_caret_pos === current_caret_pos){
|
||||||
|
self.fireEvent('goRight');
|
||||||
|
}
|
||||||
|
self.last_caret_pos = self.input.getCaretPosition();
|
||||||
|
|
||||||
self.setWidth();
|
self.setWidth();
|
||||||
self.fireEvent('change');
|
self.fireEvent('change');
|
||||||
},
|
},
|
||||||
@@ -1081,8 +1121,70 @@ Option.Choice.Tag = new Class({
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
blur: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.input.blur();
|
||||||
|
|
||||||
|
self.selected = false;
|
||||||
|
self.el.removeClass('selected');
|
||||||
|
self.input.removeEvents('outerClick');
|
||||||
|
},
|
||||||
|
|
||||||
focus: function(){
|
focus: function(){
|
||||||
this.input.focus();
|
var self = this;
|
||||||
|
if(!self.is_choice){
|
||||||
|
this.input.focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(self.selected) return;
|
||||||
|
self.selected = true;
|
||||||
|
self.el.addClass('selected');
|
||||||
|
self.input.addEvent('outerClick', self.blur.bind(self));
|
||||||
|
|
||||||
|
var temp_input = new Element('input', {
|
||||||
|
'events': {
|
||||||
|
'keyup': function(e){
|
||||||
|
e.stop();
|
||||||
|
|
||||||
|
if(e.key == 'right'){
|
||||||
|
self.fireEvent('goRight');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
else if (e.key == 'left'){
|
||||||
|
self.fireEvent('goLeft');
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
else if (e.key == 'backspace'){
|
||||||
|
self.del();
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'styles': {
|
||||||
|
'height': 0,
|
||||||
|
'width': 0,
|
||||||
|
'position': 'absolute',
|
||||||
|
'top': -200
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.el.adopt(temp_input)
|
||||||
|
temp_input.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectFrom: function(direction){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!direction || self.is_choice){
|
||||||
|
self.focus();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.focus();
|
||||||
|
var position = direction == 'left' ? 0 : self.input.get('value').length;
|
||||||
|
self.input.setCaretPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setWidth: function(){
|
setWidth: function(){
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ Page.Wanted = new Class({
|
|||||||
self.wanted = new MovieList({
|
self.wanted = new MovieList({
|
||||||
'identifier': 'wanted',
|
'identifier': 'wanted',
|
||||||
'status': 'active',
|
'status': 'active',
|
||||||
'actions': MovieActions
|
'actions': MovieActions,
|
||||||
|
'add_new': true
|
||||||
});
|
});
|
||||||
$(self.wanted).inject(self.el);
|
$(self.wanted).inject(self.el);
|
||||||
App.addEvent('library.update', self.wanted.update.bind(self.wanted));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -73,14 +73,20 @@ window.addEvent('domready', function(){
|
|||||||
new Element('option', {
|
new Element('option', {
|
||||||
'text': alt.title
|
'text': alt.title
|
||||||
}).inject(self.title_select);
|
}).inject(self.title_select);
|
||||||
|
|
||||||
|
if(alt['default'])
|
||||||
|
self.title_select.set('value', alt.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Quality.getActiveProfiles().each(function(profile){
|
Quality.getActiveProfiles().each(function(profile){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
'value': profile.id ? profile.id : profile.data.id,
|
'value': profile.id ? profile.id : profile.data.id,
|
||||||
'text': profile.label ? profile.label : profile.data.label
|
'text': profile.label ? profile.label : profile.data.label
|
||||||
}).inject(self.profile_select);
|
}).inject(self.profile_select);
|
||||||
self.profile_select.set('value', (self.movie.profile || {})['id']);
|
|
||||||
|
if(self.movie.profile)
|
||||||
|
self.profile_select.set('value', self.movie.profile.data.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ body > .spinner, .mask{
|
|||||||
border-radius:3px;
|
border-radius:3px;
|
||||||
border: 1px solid #252930;
|
border: 1px solid #252930;
|
||||||
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
|
box-shadow: inset 0 1px 0px rgba(255,255,255,0.20), 0 0 3px rgba(0,0,0, 0.2);
|
||||||
|
background: rgb(55,62,74);
|
||||||
background-image: -webkit-gradient(
|
background-image: -webkit-gradient(
|
||||||
linear,
|
linear,
|
||||||
left bottom,
|
left bottom,
|
||||||
|
|||||||
@@ -362,28 +362,29 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
.page .tag_input > ul:hover > li.choice {
|
.page .tag_input > ul:hover > li.choice {
|
||||||
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
|
background: -webkit-gradient(
|
||||||
linear,
|
linear,
|
||||||
left bottom,
|
left bottom,
|
||||||
left top,
|
left top,
|
||||||
color-stop(0, rgba(255,255,255,0.1)),
|
color-stop(0, rgba(255,255,255,0.1)),
|
||||||
color-stop(1, rgba(255,255,255,0.3))
|
color-stop(1, rgba(255,255,255,0.3))
|
||||||
);
|
);
|
||||||
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
|
background: -moz-linear-gradient(
|
||||||
center top,
|
center top,
|
||||||
rgba(255,255,255,0.3) 0%,
|
rgba(255,255,255,0.3) 0%,
|
||||||
rgba(255,255,255,0.1) 100%
|
rgba(255,255,255,0.1) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.page .tag_input > ul > li.choice:hover {
|
.page .tag_input > ul > li.choice:hover,
|
||||||
background: url('../../images/sprite.png') no-repeat 94% -53px, -webkit-gradient(
|
.page .tag_input > ul > li.choice.selected {
|
||||||
|
background: -webkit-gradient(
|
||||||
linear,
|
linear,
|
||||||
left bottom,
|
left bottom,
|
||||||
left top,
|
left top,
|
||||||
color-stop(0, #406db8),
|
color-stop(0, #406db8),
|
||||||
color-stop(1, #5b9bd1)
|
color-stop(1, #5b9bd1)
|
||||||
);
|
);
|
||||||
background: url('../../images/sprite.png') no-repeat 94% -53px, -moz-linear-gradient(
|
background: -moz-linear-gradient(
|
||||||
center top,
|
center top,
|
||||||
#5b9bd1 0%,
|
#5b9bd1 0%,
|
||||||
#406db8 100%
|
#406db8 100%
|
||||||
@@ -436,7 +437,8 @@
|
|||||||
);
|
);
|
||||||
background-size: 65%;
|
background-size: 65%;
|
||||||
}
|
}
|
||||||
.page .tag_input .choice:hover .delete { display: inline-block; }
|
.page .tag_input .choice:hover .delete,
|
||||||
|
.page .tag_input .choice.selected .delete { display: inline-block; }
|
||||||
.page .tag_input .choice .delete:hover {
|
.page .tag_input .choice .delete:hover {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
margin-top: -13px;
|
margin-top: -13px;
|
||||||
@@ -588,3 +590,8 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active .group_imdb_automation:not(.disabled) {
|
||||||
|
background: url('../../images/imdb_watchlist.png') no-repeat right 50px;
|
||||||
|
min-height: 210px;
|
||||||
|
}
|
||||||
+9
-15
@@ -31,24 +31,14 @@ load_rc_config ${name}
|
|||||||
: ${couchpotato_user:="_sabnzbd"}
|
: ${couchpotato_user:="_sabnzbd"}
|
||||||
: ${couchpotato_dir:="/usr/local/couchpotato"}
|
: ${couchpotato_dir:="/usr/local/couchpotato"}
|
||||||
: ${couchpotato_chdir:="${couchpotato_dir}"}
|
: ${couchpotato_chdir:="${couchpotato_dir}"}
|
||||||
: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"}
|
: ${couchpotato_pid:="/var/run/couchpotato.pid"}
|
||||||
|
|
||||||
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
|
|
||||||
HOST="127.0.0.1" # Set CouchPotato address here.
|
|
||||||
PORT="8081" # Set CouchPotato port here.
|
|
||||||
CPAPI="" # Set CouchPotato API key
|
|
||||||
|
|
||||||
|
pidfile="${couchpotato_pid}"
|
||||||
status_cmd="${name}_status"
|
status_cmd="${name}_status"
|
||||||
stop_cmd="${name}_stop"
|
stop_cmd="${name}_stop"
|
||||||
|
|
||||||
command="/usr/sbin/daemon"
|
command="/usr/sbin/daemon"
|
||||||
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/couchpotato.py ${couchpotato_flags}"
|
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags} --pid_file=${couchpotato_pid}"
|
||||||
|
|
||||||
# Check for wget and refuse to start without it.
|
|
||||||
if [ ! -x "${WGET}" ]; then
|
|
||||||
warn "couchpotato not started: You need wget to safely shut down CouchPotato."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure user is root when running this script.
|
# Ensure user is root when running this script.
|
||||||
if [ `id -u` != "0" ]; then
|
if [ `id -u` != "0" ]; then
|
||||||
@@ -59,19 +49,23 @@ fi
|
|||||||
verify_couchpotato_pid() {
|
verify_couchpotato_pid() {
|
||||||
# Make sure the pid corresponds to the CouchPotato process.
|
# Make sure the pid corresponds to the CouchPotato process.
|
||||||
pid=`cat ${couchpotato_pid} 2>/dev/null`
|
pid=`cat ${couchpotato_pid} 2>/dev/null`
|
||||||
ps -p ${pid} | grep -q "python ${couchpotato_dir}/couchpotato.py"
|
ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to stop CouchPotato cleanly by calling shutdown over http.
|
# Try to stop CouchPotato cleanly by calling shutdown over http.
|
||||||
couchpotato_stop() {
|
couchpotato_stop() {
|
||||||
|
|
||||||
echo "Stopping $name"
|
echo "Stopping $name"
|
||||||
verify_couchpotato_pid
|
verify_couchpotato_pid
|
||||||
${WGET} -O - -q "http://${HOST}:${PORT}/${CPAPI}/app.shutdown/" >/dev/null
|
|
||||||
if [ -n "${pid}" ]; then
|
if [ -n "${pid}" ]; then
|
||||||
|
kill -SIGTERM ${pid} 2> /dev/null
|
||||||
wait_for_pids ${pid}
|
wait_for_pids ${pid}
|
||||||
|
kill -9 ${pid} 2> /dev/null
|
||||||
echo "Stopped"
|
echo "Stopped"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
couchpotato_status() {
|
couchpotato_status() {
|
||||||
|
|||||||
Reference in New Issue
Block a user