From d0326cda7eb89dd73178f678477fc60902da1e7c Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 6 Feb 2015 19:39:10 +0800 Subject: [PATCH] Change replace HTTP auth with a login page Change to improve webserve code Add logout menu item with confirmation Add 404 error page --- CHANGES.md | 4 + autoProcessTV/autoProcessTV.py | 112 +++-- autoProcessTV/mediaToSickbeard.py | 86 ++-- gui/slick/css/dark-login.css | 98 +++++ gui/slick/css/dark.css | 30 ++ gui/slick/css/light-login.css | 85 ++++ gui/slick/css/light.css | 30 ++ gui/slick/css/style-login.css | 163 +++++++ gui/slick/css/style.css | 40 +- gui/slick/images/error16.png | Bin 0 -> 3556 bytes gui/slick/images/menu/menu-icons-black.png | Bin 3779 -> 3607 bytes gui/slick/images/menu/menu-icons-white.png | Bin 3564 -> 3679 bytes gui/slick/images/sickgear-large.png | Bin 0 -> 24318 bytes gui/slick/interfaces/default/404.tmpl | 33 ++ .../interfaces/default/config_general.tmpl | 4 +- .../default/home_trendingShows.tmpl | 2 +- gui/slick/interfaces/default/inc_top.tmpl | 7 +- gui/slick/interfaces/default/login.tmpl | 75 ++++ gui/slick/js/ajaxEpSearch.js | 2 +- gui/slick/js/confirmations.js | 33 +- sickbeard/__init__.py | 7 +- sickbeard/browser.py | 4 +- sickbeard/webapi.py | 99 +++-- sickbeard/webserve.py | 397 +++++++----------- sickbeard/webserveInit.py | 95 +++-- 25 files changed, 966 insertions(+), 440 deletions(-) create mode 100644 gui/slick/css/dark-login.css create mode 100644 gui/slick/css/light-login.css create mode 100644 gui/slick/css/style-login.css create mode 100644 gui/slick/images/error16.png create mode 100644 gui/slick/images/sickgear-large.png create mode 100644 gui/slick/interfaces/default/404.tmpl create mode 100644 gui/slick/interfaces/default/login.tmpl diff --git a/CHANGES.md b/CHANGES.md index c82dfcdd..4e19ca0f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -68,6 +68,10 @@ * Add cleansing of text used in the processes to a add a show * Add sorting of AniDB available group results * Add error handling and related UI feedback to reflect result of AniDB communications +* Change replace HTTP auth with a login page +* Change to improve webserve code +* Add logout menu item with confirmation +* Add 404 error page [develop changelog] * Change uT params from unicode to str.format as magnet URLs worked but sending files in POST bodies failed diff --git a/autoProcessTV/autoProcessTV.py b/autoProcessTV/autoProcessTV.py index 0092f9cf..95e26623 100755 --- a/autoProcessTV/autoProcessTV.py +++ b/autoProcessTV/autoProcessTV.py @@ -23,6 +23,17 @@ from __future__ import with_statement import os.path import sys +sickbeardPath = os.path.split(os.path.split(sys.argv[0])[0])[0] +sys.path.append(os.path.join(sickbeardPath, 'lib')) +sys.path.append(sickbeardPath) + +try: + import requests +except ImportError: + print ('You need to install python requests library') + sys.exit(1) + + # Try importing Python 2 modules using new names try: import ConfigParser as configparser @@ -35,77 +46,63 @@ except ImportError: import urllib.request as urllib2 from urllib.parse import urlencode -# workaround for broken urllib2 in python 2.6.5: wrong credentials lead to an infinite recursion -if sys.version_info >= (2, 6, 5) and sys.version_info < (2, 6, 6): - class HTTPBasicAuthHandler(urllib2.HTTPBasicAuthHandler): - def retry_http_basic_auth(self, host, req, realm): - # don't retry if auth failed - if req.get_header(self.auth_header, None) is not None: - return None - - return urllib2.HTTPBasicAuthHandler.retry_http_basic_auth(self, host, req, realm) - -else: - HTTPBasicAuthHandler = urllib2.HTTPBasicAuthHandler - - def processEpisode(dir_to_process, org_NZB_name=None, status=None): # Default values - host = "localhost" - port = "8081" - username = "" - password = "" + host = 'localhost' + port = '8081' + username = '' + password = '' ssl = 0 - web_root = "/" + web_root = '/' - default_url = host + ":" + port + web_root + default_url = host + ':' + port + web_root if ssl: - default_url = "https://" + default_url + default_url = 'https://' + default_url else: - default_url = "http://" + default_url + default_url = 'http://' + default_url # Get values from config_file config = configparser.RawConfigParser() - config_filename = os.path.join(os.path.dirname(sys.argv[0]), "autoProcessTV.cfg") + config_filename = os.path.join(os.path.dirname(sys.argv[0]), 'autoProcessTV.cfg') if not os.path.isfile(config_filename): - print ("ERROR: " + config_filename + " doesn\'t exist") - print ("copy /rename " + config_filename + ".sample and edit\n") - print ("Trying default url: " + default_url + "\n") + print ('ERROR: ' + config_filename + " doesn't exist") + print ('copy /rename ' + config_filename + '.sample and edit\n') + print ('Trying default url: ' + default_url + '\n') else: try: - print ("Loading config from " + config_filename + "\n") + print ('Loading config from ' + config_filename + '\n') - with open(config_filename, "r") as fp: + with open(config_filename, 'r') as fp: config.readfp(fp) # Replace default values with config_file values - host = config.get("SickBeard", "host") - port = config.get("SickBeard", "port") - username = config.get("SickBeard", "username") - password = config.get("SickBeard", "password") + host = config.get('SickBeard', 'host') + port = config.get('SickBeard', 'port') + username = config.get('SickBeard', 'username') + password = config.get('SickBeard', 'password') try: - ssl = int(config.get("SickBeard", "ssl")) + ssl = int(config.get('SickBeard', 'ssl')) except (configparser.NoOptionError, ValueError): pass try: - web_root = config.get("SickBeard", "web_root") - if not web_root.startswith("/"): - web_root = "/" + web_root + web_root = config.get('SickBeard', 'web_root') + if not web_root.startswith('/'): + web_root = '/' + web_root - if not web_root.endswith("/"): - web_root = web_root + "/" + if not web_root.endswith('/'): + web_root = web_root + '/' except configparser.NoOptionError: pass except EnvironmentError: e = sys.exc_info()[1] - print ("Could not read configuration file: " + str(e)) + print ('Could not read configuration file: ' + str(e)) # There was a config_file, don't use default values but exit sys.exit(1) @@ -121,34 +118,33 @@ def processEpisode(dir_to_process, org_NZB_name=None, status=None): params['failed'] = status if ssl: - protocol = "https://" + protocol = 'https://' else: - protocol = "http://" + protocol = 'http://' - url = protocol + host + ":" + port + web_root + "home/postprocess/processEpisode?" + urlencode(params) + url = protocol + host + ':' + port + web_root + 'home/postprocess/processEpisode' + login_url = protocol + host + ':' + port + web_root + 'login' - print ("Opening URL: " + url) + print ('Opening URL: ' + url) try: - password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() - password_mgr.add_password(None, url, username, password) - handler = HTTPBasicAuthHandler(password_mgr) - opener = urllib2.build_opener(handler) - urllib2.install_opener(opener) - - result = opener.open(url).readlines() - - for line in result: - if line: - print (line.strip()) + sess = requests.Session() + sess.post(login_url, data={'username': username, 'password': password}, stream=True, verify=False) + result = sess.get(url, params=params, stream=True, verify=False) + if result.status_code == 401: + print 'Verify and use correct username and password in autoProcessTV.cfg' + else: + for line in result.iter_lines(): + if line: + print (line.strip()) except IOError: e = sys.exc_info()[1] - print ("Unable to open URL: " + str(e)) + print ('Unable to open URL: ' + str(e)) sys.exit(1) -if __name__ == "__main__": - print ("This module is supposed to be used as import in other scripts and not run standalone.") - print ("Use sabToSickBeard instead.") +if __name__ == '__main__': + print ('This module is supposed to be used as import in other scripts and not run standalone.') + print ('Use sabToSickBeard instead.') sys.exit(1) \ No newline at end of file diff --git a/autoProcessTV/mediaToSickbeard.py b/autoProcessTV/mediaToSickbeard.py index 12ee3e69..1329424e 100755 --- a/autoProcessTV/mediaToSickbeard.py +++ b/autoProcessTV/mediaToSickbeard.py @@ -6,20 +6,24 @@ import ConfigParser import logging sickbeardPath = os.path.split(os.path.split(sys.argv[0])[0])[0] -sys.path.append(os.path.join( sickbeardPath, 'lib')) +sys.path.append(os.path.join(sickbeardPath, 'lib')) sys.path.append(sickbeardPath) -configFilename = os.path.join(sickbeardPath, "config.ini") +configFilename = os.path.join(sickbeardPath, 'config.ini') -import requests +try: + import requests +except ImportError: + print ('You need to install python requests library') + sys.exit(1) config = ConfigParser.ConfigParser() try: - fp = open(configFilename, "r") + fp = open(configFilename, 'r') config.readfp(fp) fp.close() except IOError, e: - print "Could not find/read Sickbeard config.ini: " + str(e) + print 'Could not find/read Sickbeard config.ini: ' + str(e) print 'Possibly wrong mediaToSickbeard.py location. Ensure the file is in the autoProcessTV subdir of your Sickbeard installation' time.sleep(3) sys.exit(1) @@ -28,7 +32,7 @@ scriptlogger = logging.getLogger('mediaToSickbeard') formatter = logging.Formatter('%(asctime)s %(levelname)-8s MEDIATOSICKBEARD :: %(message)s', '%b-%d %H:%M:%S') # Get the log dir setting from SB config -logdirsetting = config.get("General", "log_dir") if config.get("General", "log_dir") else 'Logs' +logdirsetting = config.get('General', 'log_dir') if config.get('General', 'log_dir') else 'Logs' # put the log dir inside the SickBeard dir, unless an absolute path logdir = os.path.normpath(os.path.join(sickbeardPath, logdirsetting)) logfile = os.path.join(logdir, 'sickbeard.log') @@ -49,7 +53,7 @@ def utorrent(): # print 'Calling utorrent' if len(sys.argv) < 2: scriptlogger.error('No folder supplied - is this being called from uTorrent?') - print "No folder supplied - is this being called from uTorrent?" + print 'No folder supplied - is this being called from uTorrent?' time.sleep(3) sys.exit() @@ -69,7 +73,7 @@ def deluge(): if len(sys.argv) < 4: scriptlogger.error('No folder supplied - is this being called from Deluge?') - print "No folder supplied - is this being called from Deluge?" + print 'No folder supplied - is this being called from Deluge?' time.sleep(3) sys.exit() @@ -82,7 +86,7 @@ def blackhole(): if None != os.getenv('TR_TORRENT_DIR'): scriptlogger.debug('Processing script triggered by Transmission') - print "Processing script triggered by Transmission" + print 'Processing script triggered by Transmission' scriptlogger.debug(u'TR_TORRENT_DIR: ' + os.getenv('TR_TORRENT_DIR')) scriptlogger.debug(u'TR_TORRENT_NAME: ' + os.getenv('TR_TORRENT_NAME')) dirName = os.getenv('TR_TORRENT_DIR') @@ -90,7 +94,7 @@ def blackhole(): else: if len(sys.argv) < 2: scriptlogger.error('No folder supplied - Your client should invoke the script with a Dir and a Relese Name') - print "No folder supplied - Your client should invoke the script with a Dir and a Relese Name" + print 'No folder supplied - Your client should invoke the script with a Dir and a Release Name' time.sleep(3) sys.exit() @@ -99,50 +103,27 @@ def blackhole(): return (dirName, nzbName) -#def sabnzb(): -# if len(sys.argv) < 2: -# scriptlogger.error('No folder supplied - is this being called from SABnzbd?') -# print "No folder supplied - is this being called from SABnzbd?" -# sys.exit() -# elif len(sys.argv) >= 3: -# dirName = sys.argv[1] -# nzbName = sys.argv[2] -# else: -# dirName = sys.argv[1] -# -# return (dirName, nzbName) -# -#def hella(): -# if len(sys.argv) < 4: -# scriptlogger.error('No folder supplied - is this being called from HellaVCR?') -# print "No folder supplied - is this being called from HellaVCR?" -# sys.exit() -# else: -# dirName = sys.argv[3] -# nzbName = sys.argv[2] -# -# return (dirName, nzbName) def main(): scriptlogger.info(u'Starting external PostProcess script ' + __file__) - host = config.get("General", "web_host") - port = config.get("General", "web_port") - username = config.get("General", "web_username") - password = config.get("General", "web_password") + host = config.get('General', 'web_host') + port = config.get('General', 'web_port') + username = config.get('General', 'web_username') + password = config.get('General', 'web_password') try: - ssl = int(config.get("General", "enable_https")) + ssl = int(config.get('General', 'enable_https')) except (ConfigParser.NoOptionError, ValueError): ssl = 0 try: - web_root = config.get("General", "web_root") + web_root = config.get('General', 'web_root') except ConfigParser.NoOptionError: - web_root = "" + web_root = '' - tv_dir = config.get("General", "tv_download_dir") - use_torrents = int(config.get("General", "use_torrents")) - torrent_method = config.get("General", "torrent_method") + tv_dir = config.get('General', 'tv_download_dir') + use_torrents = int(config.get('General', 'use_torrents')) + torrent_method = config.get('General', 'torrent_method') if not use_torrents: scriptlogger.error(u'Enable Use Torrent on Sickbeard to use this Script. Aborting!') @@ -182,28 +163,31 @@ def main(): params['nzbName'] = nzbName if ssl: - protocol = "https://" + protocol = 'https://' else: - protocol = "http://" + protocol = 'http://' if host == '0.0.0.0': host = 'localhost' - url = protocol + host + ":" + port + web_root + "/home/postprocess/processEpisode" + url = protocol + host + ':' + port + web_root + '/home/postprocess/processEpisode' + login_url = protocol + host + ':' + port + web_root + '/login' - scriptlogger.debug("Opening URL: " + url + ' with params=' + str(params)) - print "Opening URL: " + url + ' with params=' + str(params) + scriptlogger.debug('Opening URL: ' + url + ' with params=' + str(params)) + print 'Opening URL: ' + url + ' with params=' + str(params) try: - response = requests.get(url, auth=(username, password), params=params, verify=False) + sess = requests.Session() + sess.post(login_url, data={'username': username, 'password': password}, stream=True, verify=False) + response = sess.get(url, auth=(username, password), params=params, verify=False, allow_redirects=False) except Exception, e: scriptlogger.error(u': Unknown exception raised when opening url: ' + str(e)) time.sleep(3) sys.exit() if response.status_code == 401: - scriptlogger.error(u'Invalid Sickbeard Username or Password, check your config') - print 'Invalid Sickbeard Username or Password, check your config' + scriptlogger.error(u'Verify and use correct username and password in autoProcessTV.cfg') + print 'Verify and use correct username and password in autoProcessTV.cfg' time.sleep(3) sys.exit() diff --git a/gui/slick/css/dark-login.css b/gui/slick/css/dark-login.css new file mode 100644 index 00000000..53deacb3 --- /dev/null +++ b/gui/slick/css/dark-login.css @@ -0,0 +1,98 @@ +/* ======================================================================= +login.tmpl +========================================================================== */ + +.login-remember span { + color: #aaa +} + +/* ======================================================================= +Global +========================================================================== */ + +.red-text { + color: #d33; +} + +/* ======================================================================= +bootstrap Overrides +========================================================================== */ + +body { + color: #fff; + background-color: #222; +} + +input, textarea, select, .uneditable-input { + width: auto; + color: #000; +} + +.btn { + color: #fff; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75); + background-color: #2672B6; + *background-color: #2672B6; + background-image: -ms-linear-gradient(top, #297AB8, #15528F); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#297AB8), to(#15528F)); + background-image: -webkit-linear-gradient(top, #297AB8, #15528F); + background-image: -o-linear-gradient(top, #297AB8, #15528F); + background-image: linear-gradient(top, #297AB8, #15528F); + background-image: -moz-linear-gradient(top, #297AB8, #15528F); + border: 1px solid #111; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #111 #111 #111; + border-bottom-color: #111; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#297AB8', endColorstr='#15528F', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.0), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.0), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.0), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #2672B6; + *background-color: #2672B6; + color: #fff; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; + color: #fff; +} + +.btn:hover { + color: #fff; + background-color: #2672B6; + *background-color: #2672B6; + background-position: 0 -150px; + -webkit-transition: background-position 0.0s linear; + -moz-transition: background-position 0.0s linear; + -ms-transition: background-position 0.0s linear; + -o-transition: background-position 0.0s linear; + transition: background-position 0.0s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; + color: #fff; +} + +.btn.active, +.btn:active { + background-color: #2672B6; + background-color: #2672B6 \9; + background-image: none; + color: #fff; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} \ No newline at end of file diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index 230bc637..2b6a3f21 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -298,6 +298,18 @@ inc_top.tmpl background-position: -399px 0; } +.menu-icon-logout { + background-position: -420px 0; +} + +.menu-icon-kodi { + background-position: -441px 0; +} + +.menu-icon-plex { + background-position: -462px 0; +} + [class^="submenu-icon-"], [class*=" submenu-icon-"] { background: url("../images/menu/menu-icons-white.png"); height: 16px; @@ -328,6 +340,16 @@ inc_top.tmpl background-position: -378px 0; } +.submenu-icon-kodi { + background-position: -441px 0; +} + +.submenu-icon-plex { + background-position: -462px -2px; + margin-top: 2px; + height: 12px; +} + /* ======================================================================= inc_bottom.tmpl ========================================================================== */ @@ -776,6 +798,14 @@ a.whitelink { } +/* ======================================================================= +404.tmpl +========================================================================== */ + +#error-404 path { +fill: #fff; +} + /* ======================================================================= Global ========================================================================== */ diff --git a/gui/slick/css/light-login.css b/gui/slick/css/light-login.css new file mode 100644 index 00000000..f482c4c5 --- /dev/null +++ b/gui/slick/css/light-login.css @@ -0,0 +1,85 @@ +/* ======================================================================= +login.tmpl +========================================================================== */ + +.login-remember span { + color: #666 +} + +/* ======================================================================= +bootstrap Overrides +========================================================================== */ + +body { + color: #000; +} + +input, textarea, select, .uneditable-input { + width: auto; + color: #000; +} + +.btn { + color: #333333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + border: 1px solid #cccccc; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-bottom-color: #b3b3b3; + filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:hover { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} \ No newline at end of file diff --git a/gui/slick/css/light.css b/gui/slick/css/light.css index 3dc56096..ab7a5f51 100644 --- a/gui/slick/css/light.css +++ b/gui/slick/css/light.css @@ -285,6 +285,18 @@ inc_top.tmpl background-position: -399px 0; } +.menu-icon-logout { + background-position: -420px 0; +} + +.menu-icon-kodi { + background-position: -441px 0; +} + +.menu-icon-plex { + background-position: -462px 0; +} + [class^="submenu-icon-"], [class*=" submenu-icon-"] { background: url("../images/menu/menu-icons-black.png"); height: 16px; @@ -315,6 +327,16 @@ inc_top.tmpl background-position: -378px 0; } +.submenu-icon-kodi { + background-position: -441px 0; +} + +.submenu-icon-plex { + background-position: -462px -2px; + margin-top: 2px; + height: 12px; +} + /* ======================================================================= inc_bottom.tmpl ========================================================================== */ @@ -738,6 +760,14 @@ a.whitelink { color: #fff; } +/* ======================================================================= +404.tmpl +========================================================================== */ + +#error-404 path { +fill: #000; +} + /* ======================================================================= Global ========================================================================== */ diff --git a/gui/slick/css/style-login.css b/gui/slick/css/style-login.css new file mode 100644 index 00000000..0c98ae39 --- /dev/null +++ b/gui/slick/css/style-login.css @@ -0,0 +1,163 @@ +/* ======================================================================= +fonts +========================================================================== */ +/* Open Sans */ +/* Regular */ +@font-face { + font-family: 'Open Sans'; + + src:url('fonts/OpenSans-Regular-webfont.eot'); + src:url('fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('fonts/OpenSans-Regular-webfont.woff') format('woff'), + url('fonts/OpenSans-Regular-webfont.ttf') format('truetype'), + url('fonts/OpenSans-Regular-webfont.svg#OpenSansRegular') format('svg'); + font-weight: normal; + font-weight: 400; + font-style: normal; +} + +/* ======================================================================= +login.tmpl +========================================================================== */ + +.login { + position : absolute; + display: block; + height: 275px; + width: 300px; + margin-top: -137.5px; + margin-left: -150px; + top: 50%; + left: 50%; +} + +.login-img { + height: 128px; + width: 250px; + background-image: url('../images/sickgear-large.png'); + margin-bottom: 0; +} + +.login .glyphicon { + top: 0 +} + +.login-error { + height: 20px; + margin-bottom: 2px; + font-size: 13px; + text-align: center; +} + +.login-remember { + font-size: 12px; + display: inline-block; + padding: 2px 0; + line-height: 14px +} + +.login-remember input { + position: relative; + top: 2px +} + +.login-remember span { + margin-left: 5px; +} + +.error16 { + background: url('../images/error16.png') no-repeat 0 1px; + width: 16px; + display: inline-block; +} + +/* ======================================================================= +Global +========================================================================== */ + +.red-text { + color: #d33; +} + +/* ======================================================================= +bootstrap Overrides +========================================================================== */ + +body { + padding-top: 60px; + overflow-y: scroll; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #000; +} + +label { + font-weight: normal; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 10px 4px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 12px; + line-height: 16px; + *line-height: 20px; + text-align: center; + vertical-align: middle; + cursor: pointer; + background-repeat: repeat-x; + *border: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; +} + +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + *background-color: #d9d9d9; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} \ No newline at end of file diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index 7f79fa44..7b430766 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -139,7 +139,6 @@ fonts font-style: normal; } - /* ======================================================================= inc_top.tmpl ========================================================================== */ @@ -444,6 +443,18 @@ inc_top.tmpl background-position: -399px 0; } +.menu-icon-logout { + background-position: -420px 0; +} + +.menu-icon-kodi { + background-position: -441px 0; +} + +.menu-icon-plex { + background-position: -462px 0; +} + [class^="submenu-icon-"], [class*=" submenu-icon-"] { background: url("../images/menu/menu-icons-black.png"); height: 16px; @@ -474,6 +485,14 @@ inc_top.tmpl background-position: -378px 0; } +.submenu-icon-kodi { + background-position: -441px 0; +} + +.submenu-icon-plex { + background-position: -462px 0; +} + /* ======================================================================= inc_bottom.tmpl ========================================================================== */ @@ -2206,6 +2225,25 @@ a.whitelink { color: #fff; } +/* ======================================================================= +404.tmpl +========================================================================== */ + +#error-404 { + text-align: center; +} + +#error-404 h1 { + font-size: 200px; + line-height: 200px; + font-weight: 900; +} + +#error-404 h2 { + text-transform: uppercase; + font-size: 50px; + font-weight: 900; +} /* ======================================================================= Global diff --git a/gui/slick/images/error16.png b/gui/slick/images/error16.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d195276f05bcc764a2c9539659e34be1e10824 GIT binary patch literal 3556 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 literal 3779 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 tMB*0F3qUjzqd?p&=7F4 diff --git a/gui/slick/images/menu/menu-icons-white.png b/gui/slick/images/menu/menu-icons-white.png index f301d60255331d7c22808af5d395b7f7c9e3048b..0d867eca241237b105e57a2f4382b2e976979241 100644 GIT binary patch literal 3679 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 literal 3564 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 diff --git a/gui/slick/images/sickgear-large.png b/gui/slick/images/sickgear-large.png new file mode 100644 index 0000000000000000000000000000000000000000..857e2dc25072c13a6c1dec1d2c2a0eb22482c495 GIT binary patch literal 24318 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 + + + + + + + SickGear - BRANCH:[$sickbeard.BRANCH] - $title + + + + + + + + +
+

404

+

Page Not Found

+ +
+ + \ No newline at end of file diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index ac6f1f64..0d4b7e11 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -330,7 +330,7 @@
diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl index b71bde41..8c6c5b0d 100644 --- a/gui/slick/interfaces/default/inc_top.tmpl +++ b/gui/slick/interfaces/default/inc_top.tmpl @@ -91,7 +91,7 @@ \$("#SubMenu a[href$='/errorlogs/clearerrors/']").addClass('btn').html(' Clear Errors'); \$("#SubMenu a:contains('Re-scan')").addClass('btn').html(' Re-scan'); \$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html(' Backlog Overview'); - \$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html(' Update PLEX'); + \$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html(' Update PLEX'); \$("#SubMenu a:contains('Force')").addClass('btn').html(' Force Full Update'); \$("#SubMenu a:contains('Rename')").addClass('btn').html(' Preview Rename'); \$("#SubMenu a[href$='/config/subtitles/']").addClass('btn').html(' Search Subtitles'); @@ -166,7 +166,7 @@
  •  Manage Searches
  •  Episode Status Management
  • #if $sickbeard.USE_PLEX and $sickbeard.PLEX_SERVER_HOST != "": -
  •  Update PLEX
  • +
  •  Update PLEX
  • #end if #if $sickbeard.USE_XBMC and $sickbeard.XBMC_HOST != "":
  •  Update XBMC
  • @@ -211,6 +211,9 @@ System diff --git a/gui/slick/interfaces/default/login.tmpl b/gui/slick/interfaces/default/login.tmpl new file mode 100644 index 00000000..e2754d72 --- /dev/null +++ b/gui/slick/interfaces/default/login.tmpl @@ -0,0 +1,75 @@ +#import sickbeard + +#set global $title = 'Login' + +#set global $sbPath = '..' + +#set global $topmenu = 'login' +#import os.path + + + + + + + + + SickGear - BRANCH:[$sickbeard.BRANCH] - $title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/slick/js/ajaxEpSearch.js b/gui/slick/js/ajaxEpSearch.js index acb84eae..d4502673 100644 --- a/gui/slick/js/ajaxEpSearch.js +++ b/gui/slick/js/ajaxEpSearch.js @@ -1,4 +1,4 @@ -var search_status_url = sbRoot + '/getManualSearchStatus'; +var search_status_url = sbRoot + '/home/getManualSearchStatus'; PNotify.prototype.options.maxonscreen = 5; $.fn.manualSearches = []; diff --git a/gui/slick/js/confirmations.js b/gui/slick/js/confirmations.js index c6a2ea97..6ce7a10a 100644 --- a/gui/slick/js/confirmations.js +++ b/gui/slick/js/confirmations.js @@ -1,5 +1,26 @@ $(document).ready(function () { - $('a.shutdown').bind("click",function(e) { + $('a.logout').bind('click',function(e) { + e.preventDefault(); + var target = $( this ).attr('href'); + $.confirm({ + 'title' : 'Logout', + 'message' : 'Are you sure you want to Logout from SickGear ?', + 'buttons' : { + 'Yes' : { + 'class' : 'green', + 'action': function(){ + location.href = target; + } + }, + 'No' : { + 'class' : 'red', + 'action': function(){} // Nothing to do in this case. You can as well omit the action property. + } + } + }); + }); + + $('a.shutdown').bind('click',function(e) { e.preventDefault(); var target = $( this ).attr('href'); $.confirm({ @@ -20,7 +41,7 @@ $(document).ready(function () { }); }); - $('a.restart').bind("click",function(e) { + $('a.restart').bind('click',function(e) { e.preventDefault(); var target = $( this ).attr('href'); $.confirm({ @@ -41,10 +62,10 @@ $(document).ready(function () { }); }); - $('a.remove').bind("click",function(e) { + $('a.remove').bind('click',function(e) { e.preventDefault(); var target = $( this ).attr('href'); - var showname = document.getElementById("showtitle").getAttribute('data-showname'); + var showname = document.getElementById('showtitle').getAttribute('data-showname'); $.confirm({ 'title' : 'Remove Show', 'message' : 'Are you sure you want to remove ' + showname + ' from the database ?

    Check to delete files as well. IRREVERSIBLE', @@ -64,7 +85,7 @@ $(document).ready(function () { }); }); - $('a.clearhistory').bind("click",function(e) { + $('a.clearhistory').bind('click',function(e) { e.preventDefault(); var target = $( this ).attr('href'); $.confirm({ @@ -85,7 +106,7 @@ $(document).ready(function () { }); }); - $('a.trimhistory').bind("click",function(e) { + $('a.trimhistory').bind('click',function(e) { e.preventDefault(); var target = $( this ).attr('href'); $.confirm({ diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index ff02b2da..9a636fbe 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -29,6 +29,8 @@ from threading import Lock # apparently py2exe won't build these unless they're imported somewhere import sys import os.path +import uuid +import base64 sys.path.append(os.path.abspath('../lib')) from sickbeard import providers, metadata, config, webserveInit from sickbeard.providers.generic import GenericProvider @@ -450,6 +452,8 @@ CALENDAR_UNPROTECTED = False TMDB_API_KEY = 'edc5f123313769de83a71e157758030b' TRAKT_API_KEY = 'abd806c54516240c76e4ebc9c5ccf394' +COOKIE_SECRET = base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes) + __INITIALIZED__ = False def get_backlog_cycle_time(): @@ -499,7 +503,8 @@ def initialize(consoleLogging=True): USE_FAILED_DOWNLOADS, DELETE_FAILED, ANON_REDIRECT, LOCALHOST_IP, TMDB_API_KEY, DEBUG, PROXY_SETTING, PROXY_INDEXERS, \ AUTOPOSTPROCESSER_FREQUENCY, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY, \ ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \ - ANIME_SPLIT_HOME, SCENE_DEFAULT, BACKLOG_DAYS, ANIME_TREAT_AS_HDTV + ANIME_SPLIT_HOME, SCENE_DEFAULT, BACKLOG_DAYS, ANIME_TREAT_AS_HDTV, \ + COOKIE_SECRET if __INITIALIZED__: return False diff --git a/sickbeard/browser.py b/sickbeard/browser.py index a471a84f..df027315 100644 --- a/sickbeard/browser.py +++ b/sickbeard/browser.py @@ -22,7 +22,7 @@ import string from tornado.httputil import HTTPHeaders from tornado.web import RequestHandler from sickbeard import encodingKludge as ek -from sickbeard import logger +from sickbeard import logger, webserve # use the built-in if it's available (python 2.6), if not use the included library try: @@ -107,7 +107,7 @@ def foldersAtPath(path, includeParent=False, includeFiles=False): return entries -class WebFileBrowser(RequestHandler): +class WebFileBrowser(webserve.MainHandler): def index(self, path='', includeFiles=False, *args, **kwargs): self.set_header("Content-Type", "application/json") return json.dumps(foldersAtPath(path, True, bool(int(includeFiles)))) diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index 518cb30c..7770dc95 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -39,6 +39,7 @@ from sickbeard.exceptions import ex from sickbeard.common import SNATCHED, SNATCHED_PROPER, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN from sickbeard.helpers import remove_article from common import Quality, qualityPresetStrings, statusStrings +from sickbeard.webserve import MainHandler try: import json @@ -66,15 +67,27 @@ result_type_map = {RESULT_SUCCESS: "success", } # basically everything except RESULT_SUCCESS / success is bad -class Api(webserve.MainHandler): + +class Api(webserve.BaseHandler): """ api class that returns json results """ version = 4 # use an int since float-point is unpredictible intent = 4 - def index(self, *args, **kwargs): + def set_default_headers(self): + self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + + def get(self, route, *args, **kwargs): + route = route.strip('/') or 'index' + + kwargs = self.request.arguments + for arg, value in kwargs.items(): + if len(value) == 1: + kwargs[arg] = value[0] + + args = args[1:] self.apiKey = sickbeard.API_KEY - access, accessMsg, args, kwargs = self._grand_access(self.apiKey, args, kwargs) + access, accessMsg, args, kwargs = self._grand_access(self.apiKey, route, args, kwargs) # set the output callback # default json @@ -118,10 +131,43 @@ class Api(webserve.MainHandler): outputCallback = outputCallbackDict[outDict['outputType']] else: outputCallback = outputCallbackDict['default'] + self.finish(outputCallback(outDict)) - return outputCallback(outDict) + def _out_as_json(self, dict): + self.set_header('Content-Type', 'application/json') + try: + out = json.dumps(dict, indent=self.intent, sort_keys=True) + if 'jsonp' in self.request.query_arguments: + out = self.request.arguments['jsonp'] + '(' + out + ');' # wrap with JSONP call if requested - def builder(self): + except Exception, e: # if we fail to generate the output fake an error + logger.log(u'API :: ' + traceback.format_exc(), logger.DEBUG) + out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex( + e) + '"}' + + tornado_write_hack_dict = {'unwrap_json': out} + return tornado_write_hack_dict + + def _grand_access(self, realKey, apiKey, args, kwargs): + """ validate api key and log result """ + remoteIp = self.request.remote_ip + + if not sickbeard.USE_API: + msg = u'API :: ' + remoteIp + ' - SB API Disabled. ACCESS DENIED' + return False, msg, args, kwargs + elif apiKey == realKey: + msg = u'API :: ' + remoteIp + ' - gave correct API KEY. ACCESS GRANTED' + return True, msg, args, kwargs + elif not apiKey: + msg = u'API :: ' + remoteIp + ' - gave NO API KEY. ACCESS DENIED' + return False, msg, args, kwargs + else: + msg = u'API :: ' + remoteIp + ' - gave WRONG API KEY ' + apiKey + '. ACCESS DENIED' + return False, msg, args, kwargs + + +class ApiBuilder(webserve.MainHandler): + def index(self): """ expose the api-builder template """ t = webserve.PageTemplate(headers=self.request.headers, file="apiBuilder.tmpl") @@ -153,45 +199,6 @@ class Api(webserve.MainHandler): return webserve._munge(t) - def _out_as_json(self, dict): - self.set_header("Content-Type", "application/json") - try: - out = json.dumps(dict, indent=self.intent, sort_keys=True) - if 'jsonp' in self.request.query_arguments: - out = self.request.arguments['jsonp'] + '(' + out + ');' # wrap with JSONP call if requested - - except Exception, e: # if we fail to generate the output fake an error - logger.log(u"API :: " + traceback.format_exc(), logger.DEBUG) - out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex( - e) + '"}' - - tornado_write_hack_dict = {'unwrap_json': out} - return tornado_write_hack_dict - - def _grand_access(self, realKey, args, kwargs): - """ validate api key and log result """ - remoteIp = self.request.remote_ip - apiKey = kwargs.get("apikey", None) - if not apiKey: - if args: # if we have keyless vars we assume first one is the api key, always ! - apiKey = args[0] - args = args[1:] # remove the apikey from the args tuple - else: - del kwargs["apikey"] - - if not sickbeard.USE_API: - msg = u"API :: " + remoteIp + " - SB API Disabled. ACCESS DENIED" - return False, msg, args, kwargs - elif apiKey == realKey: - msg = u"API :: " + remoteIp + " - gave correct API KEY. ACCESS GRANTED" - return True, msg, args, kwargs - elif not apiKey: - msg = u"API :: " + remoteIp + " - gave NO API KEY. ACCESS DENIED" - return False, msg, args, kwargs - else: - msg = u"API :: " + remoteIp + " - gave WRONG API KEY " + apiKey + ". ACCESS DENIED" - return False, msg, args, kwargs - def call_dispatcher(handler, args, kwargs): """ calls the appropriate CMD class @@ -2191,7 +2198,7 @@ class CMD_ShowGetPoster(ApiCall): def run(self): """ get the poster for a show in sickbeard """ - return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'poster')} + return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'poster', True)} class CMD_ShowGetBanner(ApiCall): @@ -2209,7 +2216,7 @@ class CMD_ShowGetBanner(ApiCall): def run(self): """ get the banner for a show in sickbeard """ - return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'banner')} + return {'outputType': 'image', 'image': self.handler.showPoster(self.indexerid, 'banner', True)} class CMD_ShowPause(ApiCall): diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 8d30db1e..82c05862 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -60,7 +60,6 @@ from sickbeard.scene_numbering import get_scene_numbering, set_scene_numbering, from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names -from browser import WebFileBrowser from mimetypes import MimeTypes from lib.dateutil import tz @@ -82,54 +81,8 @@ except ImportError: from lib import adba from Cheetah.Template import Template -from tornado.web import RequestHandler, HTTPError, asynchronous - - -def authenticated(handler_class): - def wrap_execute(handler_execute): - def basicauth(handler, transforms, *args, **kwargs): - def _request_basic_auth(handler): - handler.set_status(401) - handler.set_header('WWW-Authenticate', 'Basic realm="SickGear"') - handler._transforms = [] - handler.finish() - return False - - try: - if not (sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD): - return True - elif (handler.request.uri.startswith(sickbeard.WEB_ROOT + '/api') and - '/api/builder' not in handler.request.uri): - return True - elif (handler.request.uri.startswith(sickbeard.WEB_ROOT + '/calendar') and - sickbeard.CALENDAR_UNPROTECTED): - return True - - auth_hdr = handler.request.headers.get('Authorization') - - if auth_hdr == None: - return _request_basic_auth(handler) - if not auth_hdr.startswith('Basic '): - return _request_basic_auth(handler) - - auth_decoded = base64.decodestring(auth_hdr[6:]) - username, password = auth_decoded.split(':', 2) - - if username != sickbeard.WEB_USERNAME or password != sickbeard.WEB_PASSWORD: - return _request_basic_auth(handler) - except Exception, e: - return _request_basic_auth(handler) - return True - - def _execute(self, transforms, *args, **kwargs): - if not basicauth(self, transforms, *args, **kwargs): - return False - return handler_execute(self, transforms, *args, **kwargs) - - return _execute - - handler_class._execute = wrap_execute(handler_class._execute) - return handler_class +from tornado.web import RequestHandler, HTTPError, asynchronous, authenticated +from tornado import gen, escape class HTTPRedirect(Exception): @@ -151,8 +104,119 @@ def redirect(url, permanent=False, status=None): raise HTTPRedirect(sickbeard.WEB_ROOT + url, permanent, status) -@authenticated -class MainHandler(RequestHandler): +class BaseHandler(RequestHandler): + def set_default_headers(self): + self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + + def redirect(self, url, permanent=False, status=None): + if not url.startswith(sickbeard.WEB_ROOT): + url = sickbeard.WEB_ROOT + url + + super(BaseHandler, self).redirect(url, permanent, status) + + def get_current_user(self, *args, **kwargs): + if sickbeard.WEB_USERNAME or sickbeard.WEB_PASSWORD: + return self.get_secure_cookie('user') + else: + return True + + def showPoster(self, show=None, which=None, api=None): + # Redirect initial poster/banner thumb to default images + if which[0:6] == 'poster': + default_image_name = 'poster.png' + else: + default_image_name = 'banner.png' + + static_image_path = os.path.join('/images', default_image_name) + if show and sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)): + cache_obj = image_cache.ImageCache() + + image_file_name = None + if which == 'poster': + image_file_name = cache_obj.poster_path(show) + if which == 'poster_thumb': + image_file_name = cache_obj.poster_thumb_path(show) + if which == 'banner': + image_file_name = cache_obj.banner_path(show) + if which == 'banner_thumb': + image_file_name = cache_obj.banner_thumb_path(show) + + if ek.ek(os.path.isfile, image_file_name): + static_image_path = image_file_name + + if api: + mime_type, encoding = MimeTypes().guess_type(static_image_path) + self.set_header('Content-Type', mime_type) + with file(static_image_path, 'rb') as img: + return img.read() + else: + static_image_path = os.path.normpath(static_image_path.replace(sickbeard.CACHE_DIR, '/cache')) + static_image_path = static_image_path.replace('\\', '/') + return redirect(static_image_path) + + +class LogoutHandler(BaseHandler): + def get(self, *args, **kwargs): + self.clear_cookie('user') + self.redirect('/login/') + + +class LoginHandler(BaseHandler): + def get(self, *args, **kwargs): + if self.get_current_user(): + self.redirect(self.get_argument('next', '/home/')) + else: + t = PageTemplate(headers=self.request.headers, file='login.tmpl') + t.resp = self.get_argument('resp', '') + self.set_status(401) + self.finish(t.respond()) + + def post(self, *args, **kwargs): + username = sickbeard.WEB_USERNAME + password = sickbeard.WEB_PASSWORD + + if (self.get_argument('username') == username or not username) \ + and (self.get_argument('password') == password or not password): + + remember_me = int(self.get_argument('remember_me', default=0) or 0) + self.set_secure_cookie('user', sickbeard.COOKIE_SECRET, expires_days=30 if remember_me > 0 else None) + + self.redirect(self.get_argument('next', '/home/')) + else: + next_arg = '&next=' + self.get_argument('next', '/home/') + self.redirect('/login?resp=authfailed' + next_arg) + + +class WebHandler(BaseHandler): + def page_not_found(self): + t = PageTemplate(headers=self.request.headers, file='404.tmpl') + return _munge(t) + + @authenticated + @gen.coroutine + def get(self, route, *args, **kwargs): + route = route.strip('/') or 'index' + try: + method = getattr(self, route) + except: + self.finish(self.page_not_found()) + else: + kwargss = self.request.arguments + for arg, value in kwargss.items(): + if len(value) == 1: + kwargss[arg] = value[0] + try: + self.finish(method(**kwargss)) + except HTTPRedirect, e: + self.redirect(e.url, e.permanent, e.status) + + post = get + + +class MainHandler(WebHandler): + def index(self): + return redirect('/home/') + def http_error_401_handler(self): """ Custom handler for 401 error """ return r''' @@ -193,102 +257,11 @@ class MainHandler(RequestHandler): """ % (error, error, trace_info, request_info)) - def _dispatch(self): - path = self.request.uri.replace(sickbeard.WEB_ROOT, '').split('?')[0] - - method = path.strip('/').split('/')[-1] - - if method == 'robots.txt': - method = 'robots_txt' - - if path.startswith('/api') and method != 'builder': - apikey = path.strip('/').split('/')[-1] - method = path.strip('/').split('/')[0] - self.request.arguments.update({'apikey': [apikey]}) - - def pred(c): - return inspect.isclass(c) and c.__module__ == pred.__module__ - - try: - klass = [cls[1] for cls in - inspect.getmembers(sys.modules[__name__], pred) + [(self.__class__.__name__, self.__class__)] if - cls[0].lower() == method.lower() or method in cls[1].__dict__.keys()][0](self.application, - self.request) - except: - klass = None - - if klass and not method.startswith('_'): - # Sanitize argument lists: - args = self.request.arguments - for arg, value in args.items(): - if len(value) == 1: - args[arg] = value[0] - - # Regular method handler for classes - func = getattr(klass, method, None) - - # Special index method handler for classes and subclasses: - if path.startswith('/api') or path.endswith('/'): - if func and getattr(func, 'index', None): - func = getattr(func(self.application, self.request), 'index', None) - elif not func: - func = getattr(klass, 'index', None) - - if callable(func): - out = func(**args) - self._headers = klass._headers - return out - - raise HTTPError(404) - - @asynchronous - def get(self, *args, **kwargs): - try: - self.finish(self._dispatch()) - except HTTPRedirect, e: - self.redirect(e.url, e.permanent, e.status) - - @asynchronous - def post(self, *args, **kwargs): - try: - self.finish(self._dispatch()) - except HTTPRedirect, e: - self.redirect(e.url, e.permanent, e.status) - def robots_txt(self, *args, **kwargs): """ Keep web crawlers out """ self.set_header('Content-Type', 'text/plain') return "User-agent: *\nDisallow: /" - def showPoster(self, show=None, which=None): - # Redirect initial poster/banner thumb to default images - if which[0:6] == 'poster': - default_image_name = 'poster.png' - else: - default_image_name = 'banner.png' - - image_path = ek.ek(os.path.join, sickbeard.PROG_DIR, 'gui', 'slick', 'images', default_image_name) - if show and sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)): - cache_obj = image_cache.ImageCache() - - image_file_name = None - if which == 'poster': - image_file_name = cache_obj.poster_path(show) - if which == 'poster_thumb': - image_file_name = cache_obj.poster_thumb_path(show) - if which == 'banner': - image_file_name = cache_obj.banner_path(show) - if which == 'banner_thumb': - image_file_name = cache_obj.banner_thumb_path(show) - - if ek.ek(os.path.isfile, image_file_name): - image_path = image_file_name - - mime_type, encoding = MimeTypes().guess_type(image_path) - self.set_header('Content-Type', mime_type) - with file(image_path, 'rb') as img: - return img.read() - def setHomeLayout(self, layout): if layout not in ('poster', 'small', 'banner', 'simple'): @@ -418,7 +391,7 @@ class MainHandler(RequestHandler): # add localtime to the dict for index, item in enumerate(sql_results): - sql_results[index]['localtime'] = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(item['airdate'], + sql_results[index]['localtime'] = sbdatetime.sbdatetime.convert_to_setting(network_timezones.parse_date_time(item['airdate'], item['airs'], item['network'])) sql_results[index]['data_show_name'] = value_maybe_article(item['show_name']) sql_results[index]['data_network'] = value_maybe_article(item['network']) @@ -438,11 +411,26 @@ class MainHandler(RequestHandler): return _munge(t) - # iCalendar (iCal) - Standard RFC 5545 - # Works with iCloud, Google Calendar and Outlook. + def _genericMessage(self, subject, message): + t = PageTemplate(headers=self.request.headers, file="genericMessage.tmpl") + t.submenu = HomeMenu() + t.subject = subject + t.message = message + return _munge(t) + + +class CalendarHandler(BaseHandler): + def get(self, *args, **kwargs): + if sickbeard.CALENDAR_UNPROTECTED or self.get_current_user(): + self.write(self.calendar()) + else: + self.set_status(401) + self.write('User authentication required') + def calendar(self, *args, **kwargs): - """ Provides a subscribeable URL for iCal subscriptions - """ + """ iCalendar (iCal) - Standard RFC 5545 + Works with iCloud, Google Calendar and Outlook. + Provides a subscribeable URL for iCal subscriptions """ logger.log(u'Receiving iCal request from %s' % self.request.remote_ip) @@ -490,14 +478,27 @@ class MainHandler(RequestHandler): # Ending the iCal return ical + 'END:VCALENDAR' - def _genericMessage(self, subject, message): - t = PageTemplate(headers=self.request.headers, file="genericMessage.tmpl") - t.submenu = HomeMenu() - t.subject = subject - t.message = message - return _munge(t) - browser = WebFileBrowser +class IsAliveHandler(BaseHandler): + def get(self, *args, **kwargs): + kwargs = self.request.arguments + if 'callback' in kwargs and '_' in kwargs: + callback, _ = kwargs['callback'][0], kwargs['_'] + else: + return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query string." + + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'text/javascript') + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Headers', 'x-requested-with') + + if sickbeard.started: + results = callback + '(' + json.dumps( + {"msg": str(sickbeard.PID)}) + ');' + else: + results = callback + '(' + json.dumps({"msg": "nope"}) + ');' + + self.write(results) class PageTemplate(Template): @@ -622,7 +623,6 @@ class ManageSearches(MainHandler): return _munge(t) - def forceVersionCheck(self, *args, **kwargs): # force a check to see if there is a new version if sickbeard.versionCheckScheduler.action.check_for_new_version(force=True): @@ -649,7 +649,6 @@ class ManageSearches(MainHandler): redirect("/manage/manageSearches/") - def forceFindPropers(self, *args, **kwargs): # force it to run the next time it looks @@ -660,7 +659,6 @@ class ManageSearches(MainHandler): redirect("/manage/manageSearches/") - def pauseBacklog(self, paused=None): if paused == "1": sickbeard.searchQueueScheduler.action.pause_backlog() # @UndefinedVariable @@ -676,7 +674,6 @@ class Manage(MainHandler): t.submenu = ManageMenu() return _munge(t) - def showEpisodeStatuses(self, indexer_id, whichStatus): status_list = [int(whichStatus)] if status_list[0] == SNATCHED: @@ -699,7 +696,6 @@ class Manage(MainHandler): return json.dumps(result) - def episodeStatuses(self, whichStatus=None): if whichStatus: @@ -744,7 +740,6 @@ class Manage(MainHandler): t.sorted_show_ids = sorted_show_ids return _munge(t) - def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): status_list = [int(oldStatus)] @@ -783,7 +778,6 @@ class Manage(MainHandler): redirect('/manage/episodeStatuses/') - def showSubtitleMissed(self, indexer_id, whichSubs): myDB = db.DBConnection() cur_show_results = myDB.select( @@ -816,7 +810,6 @@ class Manage(MainHandler): return json.dumps(result) - def subtitleMissed(self, whichSubs=None): t = PageTemplate(headers=self.request.headers, file="manage_subtitleMissed.tmpl") @@ -856,7 +849,6 @@ class Manage(MainHandler): t.sorted_show_ids = sorted_show_ids return _munge(t) - def downloadSubtitleMissed(self, *args, **kwargs): to_download = {} @@ -891,7 +883,6 @@ class Manage(MainHandler): redirect('/manage/subtitleMissed/') - def backlogShow(self, indexer_id): show_obj = helpers.findCertainShow(sickbeard.showList, int(indexer_id)) @@ -901,7 +892,6 @@ class Manage(MainHandler): redirect("/manage/backlogOverview/") - def backlogOverview(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file="manage_backlogOverview.tmpl") @@ -943,7 +933,6 @@ class Manage(MainHandler): return _munge(t) - def massEdit(self, toEdit=None): t = PageTemplate(headers=self.request.headers, file="manage_massEdit.tmpl") @@ -1067,7 +1056,6 @@ class Manage(MainHandler): return _munge(t) - def massEditSubmit(self, archive_firstmatch=None, paused=None, anime=None, sports=None, scene=None, flatten_folders=None, quality_preset=False, subtitles=None, air_by_date=None, anyQualities=[], bestQualities=[], toEdit=None, *args, @@ -1172,7 +1160,6 @@ class Manage(MainHandler): redirect("/manage/") - def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toRemove=None, toMetadata=None, toSubtitle=None): if toUpdate is not None: @@ -1291,7 +1278,6 @@ class Manage(MainHandler): redirect("/manage/") - def manageTorrents(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file="manage_torrents.tmpl") @@ -1317,7 +1303,6 @@ class Manage(MainHandler): return _munge(t) - def failedDownloads(self, limit=100, toRemove=None): myDB = db.DBConnection('failed.db') @@ -1452,11 +1437,9 @@ class ConfigGeneral(MainHandler): t.submenu = ConfigMenu return _munge(t) - def saveRootDirs(self, rootDirString=None): sickbeard.ROOT_DIRS = rootDirString - def saveAddShowDefaults(self, defaultStatus, anyQualities, bestQualities, defaultFlattenFolders, subtitles=False, anime=False, scene=False): @@ -1483,7 +1466,6 @@ class ConfigGeneral(MainHandler): sickbeard.save_config() - def generateKey(self, *args, **kwargs): """ Return a new randomized API_KEY """ @@ -1507,7 +1489,6 @@ class ConfigGeneral(MainHandler): logger.log(u"New API generated") return m.hexdigest() - def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, update_shows_on_start=None, show_update_hour=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None, use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, @@ -1609,7 +1590,6 @@ class ConfigSearch(MainHandler): t.submenu = ConfigMenu return _munge(t) - def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, sab_apikey=None, sab_category=None, sab_host=None, nzbget_username=None, nzbget_password=None, nzbget_category=None, nzbget_priority=None, nzbget_host=None, nzbget_use_https=None, @@ -1691,7 +1671,6 @@ class ConfigPostProcessing(MainHandler): t.submenu = ConfigMenu return _munge(t) - def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, xbmc_data=None, xbmc_12plus_data=None, mediabrowser_data=None, sony_ps3_data=None, wdtv_data=None, tivo_data=None, mede8er_data=None, @@ -1819,7 +1798,6 @@ class ConfigPostProcessing(MainHandler): return result - def isNamingValid(self, pattern=None, multi=None, abd=False, sports=False, anime_type=None): if pattern is None: return "invalid" @@ -1854,7 +1832,6 @@ class ConfigPostProcessing(MainHandler): else: return "invalid" - def isRarSupported(self, *args, **kwargs): """ Test Packing Support: @@ -1879,7 +1856,6 @@ class ConfigProviders(MainHandler): t.submenu = ConfigMenu return _munge(t) - def canAddNewznabProvider(self, name): if not name: @@ -1894,7 +1870,6 @@ class ConfigProviders(MainHandler): else: return json.dumps({'success': tempProvider.getID()}) - def saveNewznabProvider(self, name, url, key=''): if not name or not url: @@ -1965,7 +1940,6 @@ class ConfigProviders(MainHandler): return '1' - def canAddTorrentRssProvider(self, name, url, cookies): if not name: @@ -1985,7 +1959,6 @@ class ConfigProviders(MainHandler): else: return json.dumps({'error': errMsg}) - def saveTorrentRssProvider(self, name, url, cookies): if not name or not url: @@ -2005,7 +1978,6 @@ class ConfigProviders(MainHandler): sickbeard.torrentRssProviderList.append(newProvider) return newProvider.getID() + '|' + newProvider.configStr() - def deleteTorrentRssProvider(self, id): providerDict = dict( @@ -2022,7 +1994,6 @@ class ConfigProviders(MainHandler): return '1' - def saveProviders(self, newznab_string='', torrentrss_string='', provider_order=None, **kwargs): results = [] @@ -2578,7 +2549,6 @@ class ConfigAnime(MainHandler): t.submenu = ConfigMenu return _munge(t) - def saveAnime(self, use_anidb=None, anidb_username=None, anidb_password=None, anidb_use_mylist=None, split_home=None, anime_treat_as_hdtv=None): @@ -2603,7 +2573,6 @@ class ConfigAnime(MainHandler): redirect("/config/anime/") - class Config(MainHandler): def index(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file="config.tmpl") @@ -2694,7 +2663,6 @@ class NewHomeAddShows(MainHandler): t.submenu = HomeMenu() return _munge(t) - def getIndexerLanguages(self, *args, **kwargs): result = sickbeard.indexerApi().config['valid_languages'] @@ -2706,11 +2674,9 @@ class NewHomeAddShows(MainHandler): return json.dumps({'results': result}) - def sanitizeFileName(self, name): return helpers.sanitizeFileName(name) - def searchIndexersForShowName(self, search_term, lang="en", indexer=None): if not lang or lang == 'null': lang = "en" @@ -2742,7 +2708,6 @@ class NewHomeAddShows(MainHandler): lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] return json.dumps({'results': final_results, 'langid': lang_id}) - def massAddTable(self, rootDir=None): t = PageTemplate(headers=self.request.headers, file="home_massAddTable.tmpl") t.submenu = HomeMenu() @@ -3110,7 +3075,6 @@ class NewHomeAddShows(MainHandler): return (indexer, show_dir, indexer_id, show_name) - def addExistingShows(self, shows_to_add=None, promptForSettings=None): """ Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards @@ -3192,12 +3156,10 @@ class ErrorLogs(MainHandler): return _munge(t) - def clearerrors(self, *args, **kwargs): classes.ErrorViewer.clear() redirect("/errorlogs/") - def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): t = PageTemplate(headers=self.request.headers, file="viewlogs.tmpl") @@ -3253,24 +3215,6 @@ class ErrorLogs(MainHandler): class Home(MainHandler): - def is_alive(self, *args, **kwargs): - if 'callback' in kwargs and '_' in kwargs: - callback, _ = kwargs['callback'], kwargs['_'] - else: - return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query string." - - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header('Content-Type', 'text/javascript') - self.set_header('Access-Control-Allow-Origin', '*') - self.set_header('Access-Control-Allow-Headers', 'x-requested-with') - - if sickbeard.started: - return callback + '(' + json.dumps( - {"msg": str(sickbeard.PID)}) + ');' - else: - return callback + '(' + json.dumps({"msg": "nope"}) + ');' - - def index(self, *args, **kwargs): t = PageTemplate(headers=self.request.headers, file="home.tmpl") @@ -3309,7 +3253,6 @@ class Home(MainHandler): else: return "Unable to connect to host" - def testTorrent(self, torrent_method=None, host=None, username=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3321,7 +3264,6 @@ class Home(MainHandler): return accesMsg - def testGrowl(self, host=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3338,7 +3280,6 @@ class Home(MainHandler): else: return "Registration and Testing of growl failed " + urllib.unquote_plus(host) + pw_append - def testProwl(self, prowl_api=None, prowl_priority=0): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3348,7 +3289,6 @@ class Home(MainHandler): else: return "Test prowl notice failed" - def testBoxcar2(self, accesstoken=None, sound=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3358,7 +3298,6 @@ class Home(MainHandler): else: return "Error sending Boxcar2 notification" - def testPushover(self, userKey=None, apiKey=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3368,13 +3307,11 @@ class Home(MainHandler): else: return "Error sending Pushover notification" - def twitterStep1(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') return notifiers.twitter_notifier._get_authorization() - def twitterStep2(self, key): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3385,7 +3322,6 @@ class Home(MainHandler): else: return "Unable to verify key" - def testTwitter(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3395,7 +3331,6 @@ class Home(MainHandler): else: return "Error sending tweet" - def testXBMC(self, host=None, username=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3451,7 +3386,6 @@ class Home(MainHandler): else: return notifiers.libnotify.diagnose() - def testNMJ(self, host=None, database=None, mount=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3462,7 +3396,6 @@ class Home(MainHandler): else: return "Test failed to start the scan update" - def settingsNMJ(self, host=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3474,7 +3407,6 @@ class Home(MainHandler): else: return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - def testNMJv2(self, host=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3485,7 +3417,6 @@ class Home(MainHandler): else: return "Test notice failed to " + urllib.unquote_plus(host) - def settingsNMJv2(self, host=None, dbloc=None, instance=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3498,7 +3429,6 @@ class Home(MainHandler): return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { "dbloc": dbloc} - def testTrakt(self, api=None, username=None, password=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3508,7 +3438,6 @@ class Home(MainHandler): else: return "Test notice failed to Trakt" - def loadShowNotifyLists(self, *args, **kwargs): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3523,7 +3452,6 @@ class Home(MainHandler): data['_size'] = size return json.dumps(data) - def testEmail(self, host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3533,7 +3461,6 @@ class Home(MainHandler): else: return 'ERROR: %s' % notifiers.email_notifier.last_err - def testNMA(self, nma_api=None, nma_priority=0): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3543,7 +3470,6 @@ class Home(MainHandler): else: return "Test NMA notice failed" - def testPushalot(self, authorizationToken=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3553,7 +3479,6 @@ class Home(MainHandler): else: return "Error sending Pushalot notification" - def testPushbullet(self, api=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3563,7 +3488,6 @@ class Home(MainHandler): else: return "Error sending Pushbullet notification" - def getPushbulletDevices(self, api=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') @@ -3761,7 +3685,6 @@ class Home(MainHandler): return _munge(t) - def plotDetails(self, show, season, episode): myDB = db.DBConnection() result = myDB.select( @@ -3769,7 +3692,6 @@ class Home(MainHandler): (int(show), int(season), int(episode))) return result[0]['description'] if result else 'Episode not found.' - def sceneExceptions(self, show): exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(show) if not exceptionsList: @@ -3782,7 +3704,6 @@ class Home(MainHandler): out.append("S" + str(season) + ": " + ", ".join(names)) return "
    ".join(out) - def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[], flatten_folders=None, paused=None, directCall=False, air_by_date=None, sports=None, dvdorder=None, indexerLang=None, subtitles=None, archive_firstmatch=None, rls_ignore_words=None, @@ -3974,7 +3895,6 @@ class Home(MainHandler): redirect("/home/displayShow?show=" + show) - def deleteShow(self, show=None, full=0): if show is None: @@ -4000,7 +3920,6 @@ class Home(MainHandler): '%s' % showObj.name) redirect("/home/") - def refreshShow(self, show=None): if show is None: @@ -4022,7 +3941,6 @@ class Home(MainHandler): redirect("/home/displayShow?show=" + str(showObj.indexerid)) - def updateShow(self, show=None, force=0): if show is None: @@ -4045,7 +3963,6 @@ class Home(MainHandler): redirect("/home/displayShow?show=" + str(showObj.indexerid)) - def subtitleShow(self, show=None, force=0): if show is None: @@ -4063,7 +3980,6 @@ class Home(MainHandler): redirect("/home/displayShow?show=" + str(showObj.indexerid)) - def updateXBMC(self, showName=None): # only send update to first host in the list -- workaround for xbmc sql backend users @@ -4209,7 +4125,6 @@ class Home(MainHandler): else: redirect("/home/displayShow?show=" + show) - def testRename(self, show=None): if show is None: @@ -4255,7 +4170,6 @@ class Home(MainHandler): return _munge(t) - def doRename(self, show=None, eps=None): if show is None or eps is None: @@ -4515,7 +4429,6 @@ class Home(MainHandler): return json.dumps(result) - def retryEpisode(self, show, season, episode): # retrieve the episode object and fail if we can't get one diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 528dae4a..2d58f2e3 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -5,11 +5,12 @@ import threading import sys import sickbeard import webserve +import browser import webapi from sickbeard import logger from sickbeard.helpers import create_https_certificates -from tornado.web import Application, StaticFileHandler, RedirectHandler, HTTPError +from tornado.web import Application, StaticFileHandler, HTTPError from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop @@ -41,7 +42,7 @@ class WebServer(threading.Thread): threading.Thread.__init__(self) self.daemon = True self.alive = True - self.name = "TORNADO" + self.name = 'TORNADO' self.io_loop = io_loop or IOLoop.current() self.options = options @@ -68,12 +69,12 @@ class WebServer(threading.Thread): if not (self.https_cert and os.path.exists(self.https_cert)) or not ( self.https_key and os.path.exists(self.https_key)): if not create_https_certificates(self.https_cert, self.https_key): - logger.log(u"Unable to create CERT/KEY files, disabling HTTPS") + logger.log(u'Unable to create CERT/KEY files, disabling HTTPS') sickbeard.ENABLE_HTTPS = False self.enable_https = False if not (os.path.exists(self.https_cert) and os.path.exists(self.https_key)): - logger.log(u"Disabled HTTPS because of missing CERT and KEY files", logger.WARNING) + logger.log(u'Disabled HTTPS because of missing CERT and KEY files', logger.WARNING) sickbeard.ENABLE_HTTPS = False self.enable_https = False @@ -83,47 +84,87 @@ class WebServer(threading.Thread): autoreload=False, gzip=True, xheaders=sickbeard.HANDLE_REVERSE_PROXY, - cookie_secret='61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=' + cookie_secret=sickbeard.COOKIE_SECRET, + login_url='%s/login/' % self.options['web_root'], ) # Main Handler - self.app.add_handlers(".*$", [ - (r'%s/api/(.*)(/?)' % self.options['web_root'], webapi.Api), - (r'%s/(.*)(/?)' % self.options['web_root'], webserve.MainHandler), - (r'(.*)', webserve.MainHandler) + self.app.add_handlers('.*$', [ + (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webapi.ApiBuilder), + (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), + (r'%s/config/general(/?.*)' % self.options['web_root'], webserve.ConfigGeneral), + (r'%s/config/search(/?.*)' % self.options['web_root'], webserve.ConfigSearch), + (r'%s/config/providers(/?.*)' % self.options['web_root'], webserve.ConfigProviders), + (r'%s/config/subtitles(/?.*)' % self.options['web_root'], webserve.ConfigSubtitles), + (r'%s/config/postProcessing(/?.*)' % self.options['web_root'], webserve.ConfigPostProcessing), + (r'%s/config/notifications(/?.*)' % self.options['web_root'], webserve.ConfigNotifications), + (r'%s/config/anime(/?.*)' % self.options['web_root'], webserve.ConfigAnime), + (r'%s/config(/?.*)' % self.options['web_root'], webserve.Config), + (r'%s/errorlogs(/?.*)' % self.options['web_root'], webserve.ErrorLogs), + (r'%s/history(/?.*)' % self.options['web_root'], webserve.History), + (r'%s/home/is_alive(/?.*)' % self.options['web_root'], webserve.IsAliveHandler), + (r'%s/home/addShows(/?.*)' % self.options['web_root'], webserve.NewHomeAddShows), + (r'%s/home/postprocess(/?.*)' % self.options['web_root'], webserve.HomePostProcess), + (r'%s/home(/?.*)' % self.options['web_root'], webserve.Home), + (r'%s/manage/manageSearches(/?.*)' % self.options['web_root'], webserve.ManageSearches), + (r'%s/manage/(/?.*)' % self.options['web_root'], webserve.Manage), + (r'%s/ui(/?.*)' % self.options['web_root'], webserve.UI), + (r'%s/browser(/?.*)' % self.options['web_root'], browser.WebFileBrowser), + (r'%s(/?.*)' % self.options['web_root'], webserve.MainHandler), ]) - # Static Path Handler - self.app.add_handlers(".*$", [ - (r'%s/(favicon\.ico)' % self.options['web_root'], MultiStaticFileHandler, - {'paths': [os.path.join(self.options['data_root'], 'images/ico/favicon.ico')]}), - (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'images'), MultiStaticFileHandler, - {'paths': [os.path.join(self.options['data_root'], 'images'), - os.path.join(sickbeard.CACHE_DIR, 'images')]}), - (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'css'), MultiStaticFileHandler, - {'paths': [os.path.join(self.options['data_root'], 'css')]}), - (r'%s/%s/(.*)(/?)' % (self.options['web_root'], 'js'), MultiStaticFileHandler, - {'paths': [os.path.join(self.options['data_root'], 'js')]}), + # webui login/logout handlers + self.app.add_handlers('.*$', [ + (r'%s/login(/?)' % self.options['web_root'], webserve.LoginHandler), + (r'%s/logout(/?)' % self.options['web_root'], webserve.LogoutHandler), + ]) + + # Web calendar handler (Needed because option Unprotected calendar) + self.app.add_handlers('.*$', [ + (r'%s/calendar' % self.options['web_root'], webserve.CalendarHandler), + ]) + + # Static File Handlers + self.app.add_handlers('.*$', [ + # favicon + (r'%s/(favicon\.ico)' % self.options['web_root'], StaticFileHandler, + {'path': os.path.join(self.options['data_root'], 'images/ico/favicon.ico')}), + + # images + (r'%s/images/(.*)' % self.options['web_root'], StaticFileHandler, + {'path': os.path.join(self.options['data_root'], 'images')}), + + # cached images + (r'%s/cache/images/(.*)' % self.options['web_root'], StaticFileHandler, + {'path': os.path.join(sickbeard.CACHE_DIR, 'images')}), + + # css + (r'%s/css/(.*)' % self.options['web_root'], StaticFileHandler, + {'path': os.path.join(self.options['data_root'], 'css')}), + + # javascript + (r'%s/js/(.*)' % self.options['web_root'], StaticFileHandler, + {'path': os.path.join(self.options['data_root'], 'js')}), ]) def run(self): if self.enable_https: - protocol = "https" - self.server = HTTPServer(self.app, ssl_options={"certfile": self.https_cert, "keyfile": self.https_key}) + protocol = 'https' + self.server = HTTPServer(self.app, ssl_options={'certfile': self.https_cert, 'keyfile': self.https_key}) else: - protocol = "http" + protocol = 'http' self.server = HTTPServer(self.app) - logger.log(u"Starting SickGear on " + protocol + "://" + str(self.options['host']) + ":" + str( - self.options['port']) + "/") + logger.log(u'Starting SickGear on ' + protocol + '://' + str(self.options['host']) + ':' + str( + self.options['port']) + '/') try: self.server.listen(self.options['port'], self.options['host']) except: etype, evalue, etb = sys.exc_info() logger.log( - "Could not start webserver on %s. Excpeption: %s, Error: %s" % (self.options['port'], etype, evalue), + 'Could not start webserver on %s. Excpeption: %s, Error: %s' % (self.options['port'], etype, evalue), logger.ERROR) return @@ -131,7 +172,7 @@ class WebServer(threading.Thread): self.io_loop.start() self.io_loop.close(True) except (IOError, ValueError): - # Ignore errors like "ValueError: I/O operation on closed kqueue fd". These might be thrown during a reload. + # Ignore errors like 'ValueError: I/O operation on closed kqueue fd'. These might be thrown during a reload. pass def shutDown(self):