Change replace HTTP auth with a login page

Change to improve webserve code
Add logout menu item with confirmation
Add 404 error page
This commit is contained in:
Adam 2015-02-06 19:39:10 +08:00
parent 619aca1965
commit d0326cda7e
25 changed files with 966 additions and 440 deletions

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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);
}

View file

@ -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
========================================================================== */

View file

@ -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);
}

View file

@ -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
========================================================================== */

View file

@ -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);
}

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -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')
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SickGear - BRANCH:[$sickbeard.BRANCH] - $title</title>
<link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/bootstrap.css?$sbPID"/>
<link rel="stylesheet" type="text/css" href="$sbRoot/css/style.css?$sbPID"/>
<link rel="stylesheet" type="text/css" href="$sbRoot/css/${sickbeard.THEME_NAME}.css?$sbPID" />
</head>
<body>
<div id="error-404">
<h1>404</h1>
<h2>Page Not Found</h2>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" width="500px" height="100px" viewBox="0 0 50 10" enable-background="new 0 0 50 10" xml:space="preserve"><g><path d="M2.364 7.785H2.027C1.405 7 1.1 6.1 1.1 5.275c0-0.871 0.31-1.708 0.932-2.514h0.337 C1.843 3.6 1.6 4.4 1.6 5.275C1.582 6.1 1.8 7 2.4 7.785z"/><path d="M5.563 2.569c0 0.854-0.292 1.57-0.874 2.147c-0.577 0.57-1.288 0.855-2.131 0.855V4.931c0.645 0 1.199-0.232 1.662-0.697 c0.463-0.465 0.694-1.02 0.694-1.665H5.563z"/><path d="M8.987 3.372c0.199 0 0.4 0.1 0.5 0.213c0.142 0.1 0.2 0.3 0.2 0.511c0 0.201-0.071 0.372-0.213 0.5 S9.184 4.8 9 4.824c-0.199 0-0.369-0.071-0.511-0.213S8.262 4.3 8.3 4.096c0-0.2 0.071-0.372 0.212-0.513 C8.615 3.4 8.8 3.4 9 3.372z M8.987 4.541c0.121 0 0.224-0.043 0.31-0.131c0.087-0.087 0.13-0.192 0.13-0.314 S9.383 3.9 9.3 3.785C9.21 3.7 9.1 3.7 9 3.657c-0.122 0-0.226 0.042-0.312 0.128C8.59 3.9 8.5 4 8.5 4.1 S8.59 4.3 8.7 4.41C8.763 4.5 8.9 4.5 9 4.541z"/><path d="M10.438 4.787h2.47v2.47h-2.47V4.787z M10.637 4.986v2.072h2.072V4.986H10.637z"/><path d="M14.365 3.372c0.199 0 0.4 0.1 0.5 0.213c0.142 0.1 0.2 0.3 0.2 0.511c0 0.201-0.071 0.372-0.213 0.5 s-0.312 0.213-0.509 0.213c-0.199 0-0.369-0.071-0.511-0.213c-0.143-0.143-0.213-0.313-0.213-0.514c0-0.2 0.07-0.372 0.212-0.513 C13.993 3.4 14.2 3.4 14.4 3.372z M14.365 4.541c0.121 0 0.224-0.043 0.31-0.131c0.086-0.087 0.129-0.192 0.129-0.314 s-0.043-0.226-0.129-0.311c-0.086-0.085-0.189-0.128-0.31-0.128c-0.122 0-0.226 0.042-0.312 0.1 c-0.085 0.085-0.128 0.189-0.128 0.311s0.043 0.2 0.1 0.314C14.141 4.5 14.2 4.5 14.4 4.541z"/><path d="M17.158 5.195c0 0.897-0.303 1.709-0.91 2.438h-0.513V7.612c0.286-0.265 0.519-0.59 0.699-0.971 c0.213-0.455 0.318-0.938 0.318-1.447c0-0.506-0.105-0.985-0.318-1.438c-0.18-0.382-0.413-0.705-0.699-0.97V2.757h0.513 C16.855 3.5 17.2 4.3 17.2 5.195z"/><path d="M23.797 2.569c0 0.854-0.292 1.57-0.874 2.147c-0.577 0.57-1.288 0.855-2.131 0.855V4.931c0.645 0 1.199-0.232 1.662-0.697 c0.463-0.465 0.695-1.02 0.695-1.665H23.797z"/><path d="M31.286 7.646c-0.226-0.278-0.549-0.512-0.971-0.701c-0.47-0.21-0.964-0.315-1.483-0.315s-1.015 0.105-1.485 0.3 c-0.424 0.189-0.748 0.423-0.974 0.701h-0.021V7.188c0.294-0.294 0.679-0.524 1.153-0.69c0.437-0.149 0.879-0.224 1.326-0.224 c0.448 0 0.9 0.1 1.3 0.224c0.473 0.2 0.9 0.4 1.2 0.69v0.458H31.286z"/><path d="M38.286 5.561h-5.357V4.92h2.352V2.569h0.644V4.92h2.362V5.561z"/><path d="M43.638 5.566h-5.352V4.925h5.352V5.566z"/><path d="M49 5.561h-5.357V4.92h2.352V2.569h0.644V4.92H49V5.561z"/></g></svg>
</div>
</body>
</html>

View file

@ -330,7 +330,7 @@
<div class="field-pair">
<label for="web_username">
<span class="component-title">HTTP username</span>
<span class="component-title">Username</span>
<span class="component-desc">
<input type="text" name="web_username" id="web_username" value="$sickbeard.WEB_USERNAME" class="form-control input-sm input300">
<p>blank = disable SickGear login</p>
@ -340,7 +340,7 @@
<div class="field-pair">
<label for="web_password">
<span class="component-title">HTTP password</span>
<span class="component-title">Password</span>
<span class="component-desc">
<input type="password" name="web_password" id="web_password" value="$sickbeard.WEB_PASSWORD" class="form-control input-sm input300">
<p>blank = no authentication</p>

View file

@ -150,7 +150,7 @@
<p style="line-height: 1.5; padding: 2px 5px 3px" title="<%= '%s added' % ('TVRage', 'theTVDB')['1' == cur_show['show_id'][:1]] %>">In library</p>
#else
#set $encoded_show_title = urllib.quote($cur_show['title'].encode("utf-8"))
<a href="$sbRoot/home/addTraktShow?indexer_id=${cur_show['show_id']}&amp;showName=${encoded_show_title}" class="btn btn-xs">Add Show</a>
<a href="$sbRoot/home/addShows/addTraktShow?indexer_id=${cur_show['show_id']}&amp;showName=${encoded_show_title}" class="btn btn-xs">Add Show</a>
#end if
</div>
</div>

View file

@ -91,7 +91,7 @@
\$("#SubMenu a[href$='/errorlogs/clearerrors/']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors');
\$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan');
\$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Backlog Overview');
\$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Update PLEX');
\$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html('<span class="submenu-icon-plex pull-left"></span> Update PLEX');
\$("#SubMenu a:contains('Force')").addClass('btn').html('<span class="ui-icon ui-icon-transfer-e-w pull-left"></span> Force Full Update');
\$("#SubMenu a:contains('Rename')").addClass('btn').html('<span class="ui-icon ui-icon-tag pull-left"></span> Preview Rename');
\$("#SubMenu a[href$='/config/subtitles/']").addClass('btn').html('<span class="ui-icon ui-icon-comment pull-left"></span> Search Subtitles');
@ -166,7 +166,7 @@
<li><a href="$sbRoot/manage/manageSearches/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-manage-searches"></i>&nbsp;Manage Searches</a></li>
<li><a href="$sbRoot/manage/episodeStatuses/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-backlog"></i>&nbsp;Episode Status Management</a></li>
#if $sickbeard.USE_PLEX and $sickbeard.PLEX_SERVER_HOST != "":
<li><a href="$sbRoot/home/updatePLEX/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-backlog-view"></i>&nbsp;Update PLEX</a></li>
<li><a href="$sbRoot/home/updatePLEX/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-plex"></i>&nbsp;Update PLEX</a></li>
#end if
#if $sickbeard.USE_XBMC and $sickbeard.XBMC_HOST != "":
<li><a href="$sbRoot/home/updateXBMC/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-xbmc"></i>&nbsp;Update XBMC</a></li>
@ -211,6 +211,9 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#"><img src="$sbRoot/images/menu/system18-2.png" class="navbaricon hidden-xs" /><b class="caret hidden-xs"></b><span class="visible-xs">System <b class="caret"></b></span></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/manage/manageSearches/forceVersionCheck" tabindex="$tab#set $tab += 1#"><i class="menu-icon-update"></i>&nbsp;Force Version Check</a></li>
#if $sickbeard.WEB_USERNAME or $sickbeard.WEB_PASSWORD:
<li><a href="$sbRoot/logout" class="confirm logout" tabindex="$tab#set $tab += 1#"><i class="menu-icon-logout"></i>&nbsp;Logout</a></li>
#end if
<li><a href="$sbRoot/home/restart/?pid=$sbPID" class="confirm restart" tabindex="$tab#set $tab += 1#"><i class="menu-icon-restart"></i>&nbsp;Restart</a></li>
<li><a href="$sbRoot/home/shutdown/?pid=$sbPID" class="confirm shutdown" tabindex="$tab#set $tab += 1#"><i class="menu-icon-shutdown"></i>&nbsp;Shutdown</a></li>
</ul>

View file

@ -0,0 +1,75 @@
#import sickbeard
#set global $title = 'Login'
#set global $sbPath = '..'
#set global $topmenu = 'login'
#import os.path
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SickGear - BRANCH:[$sickbeard.BRANCH] - $title</title>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<link rel="shortcut icon" href="$sbRoot/images/ico/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="$sbRoot/images/ico/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="152x152" href="$sbRoot/images/ico/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="144x144" href="$sbRoot/images/ico/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="120x120" href="$sbRoot/images/ico/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="114x114" href="$sbRoot/images/ico/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="76x76" href="$sbRoot/images/ico/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="72x72" href="$sbRoot/images/ico/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="60x60" href="$sbRoot/images/ico/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="57x57" href="$sbRoot/images/ico/apple-touch-icon-57x57.png">
<link rel="icon" type="image/png" href="$sbRoot/images/ico/favicon-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="$sbRoot/images/ico/favicon-160x160.png" sizes="160x160">
<link rel="icon" type="image/png" href="$sbRoot/images/ico/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="$sbRoot/images/ico/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="$sbRoot/images/ico/favicon-16x16.png" sizes="16x16">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="msapplication-TileImage" content="$sbRoot/images/ico/mstile-144x144.png">
<meta name="msapplication-config" content="$sbRoot/css/browserconfig.xml">
<link rel="stylesheet" type="text/css" href="$sbRoot/css/lib/bootstrap.css?$sbPID"/>
<link rel="stylesheet" type="text/css" href="$sbRoot/css/style-login.css?$sbPID"/>
<link rel="stylesheet" type="text/css" href="$sbRoot/css/${sickbeard.THEME_NAME}-login.css?$sbPID" />
</head>
<body>
<div class="login">
<form action="" method="post">
<div class="login-img center-block form-group"></div>
<div class="login-error">
<span class="#if 'authfailed'==$resp then 'showme red-text' else 'hide' #"><i class="error16">&nbsp;</i> Authentication failed, please retry</span>
</div>
<div class="form-group input-group">
<span class="input-group-addon glyphicon glyphicon-user"></span>
<input name="username" class="form-control" placeholder="Username" type="text" autofocus required>
</div>
<div class="form-group input-group">
<span class="input-group-addon glyphicon glyphicon-lock"></span>
<input name="password" class="form-control" placeholder="Password" type="password">
</div>
<div class="form-group">
<label for="remember_me" class="login-remember"><input id="remember_me" name="remember_me" type="checkbox" value="1" checked="checked"><span>Remember me</span></label>
<input class="btn pull-right" name="submit" type="submit" value="Login">
</div>
</form>
</div>
</body>
</html>

View file

@ -1,4 +1,4 @@
var search_status_url = sbRoot + '/getManualSearchStatus';
var search_status_url = sbRoot + '/home/getManualSearchStatus';
PNotify.prototype.options.maxonscreen = 5;
$.fn.manualSearches = [];

View file

@ -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 <span class="footerhighlight">' + showname + '</span> from the database ?<br /><br /><input type="checkbox" id="deleteFiles"> <span class="red-text">Check to delete files as well. IRREVERSIBLE</span></input>',
@ -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({

View file

@ -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

View file

@ -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))))

View file

@ -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):

View file

@ -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'''<!DOCTYPE html>
@ -193,102 +257,11 @@ class MainHandler(RequestHandler):
</html>""" % (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 <http://tools.ietf.org/html/rfc5546>
# 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 <http://tools.ietf.org/html/rfc5546>
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 "<br/>".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):
'<b>%s</b>' % 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

View file

@ -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):