Commit 126f2677 authored by echel0n's avatar echel0n
Browse files

Merge branch 'release/9.3.59'

parents 6a9bab78 fb31d879
# Changelog
- * 68b3fea - 2018-08-23: Release v9.3.58
- * ba44b3c - 2018-08-24: Release v9.3.59
- * 2a1d2ce - 2018-08-24: Pre-Release v9.3.59.dev3
- * b18bd58 - 2018-08-24: Misc improvements made to websockets code
- * a1cc57d - 2018-08-24: Pre-Release v9.3.59.dev2
- * 30e4803 - 2018-08-24: Misc improvements made to websockets code
- * 5ddb5de - 2018-08-24: Pre-Release v9.3.59.dev1
- * c0b6be8 - 2018-08-24: Added websockets support for ui messages
- * 096b055 - 2018-08-24: Improvements made to tornado web handler code
- * a7f3541 - 2018-08-23: Release v9.3.58
- * 0366d15 - 2018-08-23: Changed the way TheTVDB cache's its shows, improved performance of app
- * faefa9d - 2018-08-23: Fixed issues with custom web roots and redirects Checking for updates now redirects back to referrer url
- * bd35aae - 2018-08-23: Pre-Release v9.3.58.dev2
......
......@@ -80,7 +80,7 @@ class Core(object):
def __init__(self):
self.started = False
self.daemon = None
self.io_loop = IOLoop().current()
self.io_loop = IOLoop()
self.pid = os.getpid()
self.showlist = []
......
......@@ -21,6 +21,7 @@ from __future__ import unicode_literals
import datetime
import sickrage
from sickrage.core.websocket import WebSocketMessage
MESSAGE = 'notice'
ERROR = 'error'
......@@ -35,7 +36,7 @@ class Notifications(object):
self._messages = []
self._errors = []
def message(self, title, message=None):
def message(self, title, message=""):
"""
Add a regular notification to the queue
......@@ -43,9 +44,11 @@ class Notifications(object):
message: The message portion of the notification
"""
self._messages.append(Notification(title, message, MESSAGE))
n = Notification(title, message, MESSAGE)
if not WebSocketMessage('notification', n.data).push():
self._messages.append(n)
def error(self, title, message=None):
def error(self, title, message=""):
"""
Add an error notification to the queue
......@@ -53,7 +56,9 @@ class Notifications(object):
message: The message portion of the notification
"""
self._errors.append(Notification(title, message, ERROR))
n = Notification(title, message, ERROR)
if not WebSocketMessage('notification', n.data).push():
self._errors.append(n)
def get_notifications(self, remote_ip='127.0.0.1'):
"""
......@@ -86,9 +91,17 @@ class Notification(object):
self._when = datetime.datetime.now()
self._seen = []
self.type = type or MESSAGE
self._type = type or MESSAGE
self._timeout = timeout or datetime.timedelta(minutes=1)
@property
def data(self):
return {
'title': self.title,
'body': self.message,
'type': self._type
}
def is_new(self, remote_ip='127.0.0.1'):
"""
Returns True if the notification hasn't been displayed to the current client (aka IP address).
......
......@@ -32,6 +32,7 @@ from sickrage.core.helpers import create_https_certificates, launch_browser
from sickrage.core.webserver.api import ApiHandler, KeyHandler
from sickrage.core.webserver.routes import Route
from sickrage.core.webserver.views import CalendarHandler, LoginHandler, LogoutHandler
from sickrage.core.websocket import WebSocketUIHandler
class StaticImageHandler(StaticFileHandler):
......@@ -81,7 +82,8 @@ class WebServer(object):
# web root
if sickrage.app.config.web_root:
sickrage.app.config.web_root = sickrage.app.config.web_root = ('/' + sickrage.app.config.web_root.lstrip('/').strip('/'))
sickrage.app.config.web_root = sickrage.app.config.web_root = (
'/' + sickrage.app.config.web_root.lstrip('/').strip('/'))
# api root
self.api_root = r'%s/api/%s' % (sickrage.app.config.web_root, sickrage.app.config.api_key)
......@@ -105,73 +107,82 @@ class WebServer(object):
# Load the app
self.app = Application(
[
# api
(r'%s(/?.*)' % self.api_root, ApiHandler),
debug=True,
autoreload=False,
gzip=sickrage.app.config.web_use_gzip,
cookie_secret=sickrage.app.config.web_cookie_secret,
login_url='%s/login/' % sickrage.app.config.web_root)
# redirect to home
(r"(%s)" % sickrage.app.config.web_root, RedirectHandler,
{"url": "%s/home" % sickrage.app.config.web_root}),
# Websocket handler
self.app.add_handlers(".*$", [
(r'%s/ws/ui' % sickrage.app.config.web_root, WebSocketUIHandler)
])
# api key
(r'%s/getkey(/?.*)' % sickrage.app.config.web_root, KeyHandler),
# Static File Handlers
self.app.add_handlers('.*$', [
# api
(r'%s(/?.*)' % self.api_root, ApiHandler),
# api builder
(r'%s/api/builder' % sickrage.app.config.web_root, RedirectHandler,
{"url": sickrage.app.config.web_root + '/apibuilder/'}),
# redirect to home
(r"(%s)" % sickrage.app.config.web_root, RedirectHandler,
{"url": "%s/home" % sickrage.app.config.web_root}),
# login
(r'%s/login(/?)' % sickrage.app.config.web_root, LoginHandler),
# api key
(r'%s/getkey(/?.*)' % sickrage.app.config.web_root, KeyHandler),
# logout
(r'%s/logout(/?)' % sickrage.app.config.web_root, LogoutHandler),
# api builder
(r'%s/api/builder' % sickrage.app.config.web_root, RedirectHandler,
{"url": sickrage.app.config.web_root + '/apibuilder/'}),
# calendar
(r'%s/calendar' % sickrage.app.config.web_root, CalendarHandler),
# login
(r'%s/login(/?)' % sickrage.app.config.web_root, LoginHandler),
# favicon
(r'%s/(favicon\.ico)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'images/favicon.ico')}),
# logout
(r'%s/logout(/?)' % sickrage.app.config.web_root, LogoutHandler),
# images
(r'%s/images/(.*)' % sickrage.app.config.web_root, StaticImageHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'images')}),
# calendar
(r'%s/calendar' % sickrage.app.config.web_root, CalendarHandler),
# css
(r'%s/css/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'css')}),
# favicon
(r'%s/(favicon\.ico)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'images/favicon.ico')}),
# scss
(r'%s/scss/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'scss')}),
# images
(r'%s/images/(.*)' % sickrage.app.config.web_root, StaticImageHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'images')}),
# fonts
(r'%s/fonts/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'fonts')}),
# css
(r'%s/css/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'css')}),
# javascript
(r'%s/js/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'js')}),
# scss
(r'%s/scss/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'scss')}),
# videos
(r'%s/videos/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": self.video_root}),
] + Route.get_routes(sickrage.app.config.web_root),
debug=True,
autoreload=False,
gzip=sickrage.app.config.web_use_gzip,
cookie_secret=sickrage.app.config.web_cookie_secret,
login_url='%s/login/' % sickrage.app.config.web_root)
# fonts
(r'%s/fonts/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'fonts')}),
# javascript
(r'%s/js/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": os.path.join(sickrage.app.config.gui_static_dir, 'js')}),
# videos
(r'%s/videos/(.*)' % sickrage.app.config.web_root, StaticNoCacheFileHandler,
{"path": self.video_root}),
])
self.server = HTTPServer(self.app, no_keep_alive=True, xheaders=sickrage.app.config.handle_reverse_proxy)
# Web Handlers
self.app.add_handlers('.*$', Route.get_routes(sickrage.app.config.web_root))
self.server = HTTPServer(self.app, xheaders=sickrage.app.config.handle_reverse_proxy)
if sickrage.app.config.enable_https: self.server.ssl_options = {
"certfile": sickrage.app.config.https_cert,
"keyfile": sickrage.app.config.https_key
}
try:
self.server.listen(sickrage.app.config.web_port, None)
self.server.listen(sickrage.app.config.web_port)
sickrage.app.log.info(
"SiCKRAGE :: STARTED")
......@@ -183,7 +194,8 @@ class WebServer(object):
"SiCKRAGE :: DATABASE:[v{}]".format(sickrage.app.main_db.version))
sickrage.app.log.info(
"SiCKRAGE :: URL:[{}://{}:{}{}]".format(('http', 'https')[sickrage.app.config.enable_https],
sickrage.app.config.web_host, sickrage.app.config.web_port, sickrage.app.config.web_root))
sickrage.app.config.web_host, sickrage.app.config.web_port,
sickrage.app.config.web_root))
# launch browser window
if all([not sickrage.app.no_launch,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -114,14 +114,12 @@ class BaseHandler(RequestHandler):
url = url[len(sickrage.app.config.web_root) + 1:]
if url[:3] != 'api':
return self.finish(self.render(
self.write(self.render(
'/errors/404.mako',
title=_('HTTP Error 404'),
header=_('HTTP Error 404'))
)
header=_('HTTP Error 404')))
else:
self.write('Wrong API key used')
elif self.settings.get("debug") and "exc_info" in kwargs:
exc_info = kwargs["exc_info"]
trace_info = ''.join(["%s<br>" % line for line in traceback.format_exception(*exc_info)])
......@@ -131,21 +129,21 @@ class BaseHandler(RequestHandler):
self.set_header('Content-Type', 'text/html')
self.write("""<html>
<title>{error}</title>
<body>
<button onclick="window.location='{webroot}/logs/';">View Log(Errors)</button>
<button onclick="window.location='{webroot}/home/restart?force=1';">Restart SiCKRAGE</button>
<h2>Error</h2>
<p>{error}</p>
<h2>Traceback</h2>
<p>{traceback}</p>
<h2>Request Info</h2>
<p>{request}</p>
</body>
</html>""".format(error=error,
traceback=trace_info,
request=request_info,
webroot=sickrage.app.config.web_root))
<title>{error}</title>
<body>
<button onclick="window.location='{webroot}/logs/';">View Log(Errors)</button>
<button onclick="window.location='{webroot}/home/restart?force=1';">Restart SiCKRAGE</button>
<h2>Error</h2>
<p>{error}</p>
<h2>Traceback</h2>
<p>{traceback}</p>
<h2>Request Info</h2>
<p>{request}</p>
</body>
</html>""".format(error=error,
traceback=trace_info,
request=request_info,
webroot=sickrage.app.config.web_root))
def get_current_user(self):
user = self.get_secure_cookie('sr_user')
......@@ -198,7 +196,7 @@ class BaseHandler(RequestHandler):
return self.render_string(template_name, **kwargs)
@run_on_executor
def route(self, function, **kwargs):
def worker(self, function, **kwargs):
threading.currentThread().setName("TORNADO")
kwargs = recursive_unicode(kwargs)
for arg, value in kwargs.items():
......@@ -229,17 +227,24 @@ class WebHandler(BaseHandler):
@coroutine
@authenticated
def prepare(self, *args, **kwargs):
def get(self, *args, **kwargs):
result = yield self.route()
if result: self.write(result)
@coroutine
@authenticated
def post(self, *args, **kwargs):
result = yield self.route()
if result: self.write(result)
def route(self):
# route -> method obj
method = getattr(
self, self.request.path.strip('/').split('/')[::-1][0].replace('.', '_'),
getattr(self, 'index', None)
)
if method:
result = yield self.route(method, **self.request.arguments)
if self.request.method == 'GET' or result:
self.finish(result)
if method: return self.worker(method, **self.request.arguments)
def _genericMessage(self, subject, message):
return self.render(
......@@ -291,7 +296,6 @@ class LogoutHandler(BaseHandler):
self.clear_all_cookies()
return self.redirect('/login/')
class CalendarHandler(BaseHandler):
def prepare(self, *args, **kwargs):
if sickrage.app.config.calendar_unprotected:
......@@ -525,32 +529,6 @@ class WebRoot(WebHandler):
sickrage.app.quicksearch_cache.get_shows(term) + sickrage.app.quicksearch_cache.get_episodes(term))
@Route('/ui(/?.*)')
class UI(WebHandler):
def __init__(self, *args, **kwargs):
super(UI, self).__init__(*args, **kwargs)
self.set_header('Content-Type', 'application/json')
@staticmethod
def add_message():
sickrage.app.alerts.message('Test 1', 'This is test number 1')
sickrage.app.alerts.error('Test 2', 'This is test number 2')
return "ok"
def get_messages(self):
messages = {}
cur_notification_num = 0
for cur_notification in sickrage.app.alerts.get_notifications(self.request.remote_ip):
cur_notification_num += 1
messages['notification-{}'.format(cur_notification_num)] = {
'title': cur_notification.title,
'message': cur_notification.message or "",
'type': cur_notification.type
}
return json_encode(messages)
@Route('/browser(/?.*)')
class WebFileBrowser(WebHandler):
def __init__(self, *args, **kwargs):
......
......@@ -67,7 +67,6 @@
<link rel="stylesheet" type="text/css" href="${srWebRoot}/css/core.min.css"/>
<%block name="css" />
</head>
<body data-controller="${controller}" data-action="${action}">
${mainModals()}
......
import json
from tornado.websocket import WebSocketHandler
import sickrage
clients = set()
class WebSocketUIHandler(WebSocketHandler):
"""WebSocket handler to send and receive data to and from a web client."""
def check_origin(self, origin):
"""Allow alternate origins."""
return True
def open(self, *args, **kwargs):
"""Client connected to the WebSocket."""
clients.add(self)
for n in sickrage.app.alerts.get_notifications(self.request.remote_ip):
self.write_message(WebSocketMessage('notification', n.data).json())
def on_message(self, message):
"""Received a message from the client."""
sickrage.app.log.debug('WebSocket received message from {}: {}'.format(self.request.remote_ip, message))
def data_received(self, chunk):
"""Received a streamed data chunk from the client."""
super(WebSocketUIHandler, self).data_received(chunk)
def on_close(self):
"""Client disconnected from the WebSocket."""
clients.remove(self)
def __repr__(self):
"""Client representation."""
return '<{} Client: {}>'.format(type(self).__name__, self.request.remote_ip)
class WebSocketMessage(object):
"""Represents a WebSocket message."""
def __init__(self, event, data):
"""
Construct a new WebSocket message.
:param event: A string representing the type of message (e.g. notification)
:param data: A JSON-serializable object containing the message data.
"""
self.event = event
self.data = data
@property
def content(self):
"""Get the message content."""
return {
'event': self.event,
'data': self.data
}
def json(self):
"""Return the message content as a JSON-serialized string."""
return json.dumps(self.content)
def push(self):
"""Push the message to all connected WebSocket clients."""
if not clients:
return
for client in clients:
sickrage.app.io_loop.add_callback(client.write_message, self.json())
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment