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 00000000..d7d19527 Binary files /dev/null and b/gui/slick/images/error16.png differ diff --git a/gui/slick/images/menu/menu-icons-black.png b/gui/slick/images/menu/menu-icons-black.png index 0e3cca99..43dd426c 100644 Binary files a/gui/slick/images/menu/menu-icons-black.png and b/gui/slick/images/menu/menu-icons-black.png differ diff --git a/gui/slick/images/menu/menu-icons-white.png b/gui/slick/images/menu/menu-icons-white.png index f301d602..0d867eca 100644 Binary files a/gui/slick/images/menu/menu-icons-white.png and b/gui/slick/images/menu/menu-icons-white.png differ diff --git a/gui/slick/images/sickgear-large.png b/gui/slick/images/sickgear-large.png new file mode 100644 index 00000000..857e2dc2 Binary files /dev/null and b/gui/slick/images/sickgear-large.png differ diff --git a/gui/slick/interfaces/default/404.tmpl b/gui/slick/interfaces/default/404.tmpl new file mode 100644 index 00000000..42da8b52 --- /dev/null +++ b/gui/slick/interfaces/default/404.tmpl @@ -0,0 +1,33 @@ +#import sickbeard + +#set global $title = '404' + +#set global $sbPath = '..' + +#set global $topmenu = '404' +#import os.path +#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl') + + + + + + + + + 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):