diff --git a/.gitignore b/.gitignore index 78c1cef926a87528ce6f1902a6200f43e784aeca..8841fd4166c10f411d50e8b5aae5f62ce0c761b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.pyc cache/* -cache.db +cache.db* config.ini Logs/* sickbeard.db* diff --git a/SickBeard.py b/SickBeard.py index ba46efc0a3ab8e055b797fc723d64c3aa261e280..6aab1d93aecb500daa6b2dcd8470e0801e2f44dd 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -1,165 +1,172 @@ -#!/usr/bin/env python -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -import sys - -# we only need this for compiling an EXE and I will just always do that on 2.6+ -if sys.hexversion >= 0x020600F0: - from multiprocessing import Process, freeze_support - -import locale -import os -import os.path -import threading -import time -import signal -import sqlite3 -import traceback -import getopt - -import sickbeard - -from sickbeard import db -from sickbeard.tv import TVShow -from sickbeard import logger -from sickbeard.common import * -from sickbeard.version import SICKBEARD_VERSION - -from sickbeard.webserveInit import initWebServer - -from lib.configobj import ConfigObj - -signal.signal(signal.SIGINT, sickbeard.sig_handler) -signal.signal(signal.SIGTERM, sickbeard.sig_handler) - -def loadShowsFromDB(): - - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_shows") - - for sqlShow in sqlResults: - try: - curShow = TVShow(int(sqlShow["tvdb_id"])) - sickbeard.showList.append(curShow) - except Exception, e: - logger.log(u"There was an error creating the show in "+sqlShow["location"]+": "+str(e).decode('utf-8'), logger.ERROR) - logger.log(traceback.format_exc(), logger.DEBUG) - - #TODO: make it update the existing shows if the showlist has something in it - -def daemonize(): - # Make a non-session-leader child process - try: - pid = os.fork() - if pid != 0: - sys.exit(0) - except OSError, e: - raise RuntimeError("1st fork failed: %s [%d]" % - (e.strerror, e.errno)) - - os.chdir(sickbeard.PROG_DIR) - os.setsid() - - # Make sure I can read my own files and shut out others - prev = os.umask(0) - os.umask(prev and int('077',8)) - - # Make the child a session-leader by detaching from the terminal - try: - pid = os.fork() - if pid != 0: - sys.exit(0) - except OSError, e: - raise RuntimeError("2st fork failed: %s [%d]" % - (e.strerror, e.errno)) - raise Exception, "%s [%d]" % (e.strerror, e.errno) - - dev_null = file('/dev/null', 'r') - os.dup2(dev_null.fileno(), sys.stdin.fileno()) - +#!/usr/bin/env python +# Author: Nic Wolfe <nic@wolfeden.ca> +# URL: http://code.google.com/p/sickbeard/ +# +# This file is part of Sick Beard. +# +# Sick Beard is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Sick Beard is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. + +import sys + +# we only need this for compiling an EXE and I will just always do that on 2.6+ +if sys.hexversion >= 0x020600F0: + from multiprocessing import freeze_support + +import locale +import os +import threading +import time +import signal +import traceback +import getopt + +import sickbeard + +from sickbeard import db +from sickbeard.tv import TVShow +from sickbeard import logger +from sickbeard.version import SICKBEARD_VERSION + +from sickbeard.webserveInit import initWebServer + +from lib.configobj import ConfigObj + +signal.signal(signal.SIGINT, sickbeard.sig_handler) +signal.signal(signal.SIGTERM, sickbeard.sig_handler) + +def loadShowsFromDB(): + + myDB = db.DBConnection() + sqlResults = myDB.select("SELECT * FROM tv_shows") + + for sqlShow in sqlResults: + try: + curShow = TVShow(int(sqlShow["tvdb_id"])) + sickbeard.showList.append(curShow) + except Exception, e: + logger.log(u"There was an error creating the show in "+sqlShow["location"]+": "+str(e).decode('utf-8'), logger.ERROR) + logger.log(traceback.format_exc(), logger.DEBUG) + + #TODO: make it update the existing shows if the showlist has something in it + +def daemonize(): + # Make a non-session-leader child process + try: + pid = os.fork() #@UndefinedVariable - only available in UNIX + if pid != 0: + sys.exit(0) + except OSError, e: + raise RuntimeError("1st fork failed: %s [%d]" % + (e.strerror, e.errno)) + + os.setsid() #@UndefinedVariable - only available in UNIX + + # Make sure I can read my own files and shut out others + prev = os.umask(0) + os.umask(prev and int('077',8)) + + # Make the child a session-leader by detaching from the terminal + try: + pid = os.fork() #@UndefinedVariable - only available in UNIX + if pid != 0: + sys.exit(0) + except OSError, e: + raise RuntimeError("2st fork failed: %s [%d]" % + (e.strerror, e.errno)) + raise Exception, "%s [%d]" % (e.strerror, e.errno) + + dev_null = file('/dev/null', 'r') + os.dup2(dev_null.fileno(), sys.stdin.fileno()) + if sickbeard.CREATEPID: pid = str(os.getpid()) logger.log(u"Writing PID " + pid + " to " + str(sickbeard.PIDFILE)) file(sickbeard.PIDFILE, 'w').write("%s\n" % pid) -def main(): - - # do some preliminary stuff - sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(sys.argv[0])) - sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) - sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME) - sickbeard.MY_ARGS = sys.argv[1:] +def main(): + + # do some preliminary stuff + sickbeard.MY_FULLNAME = os.path.normpath(os.path.abspath(sys.argv[0])) + sickbeard.MY_NAME = os.path.basename(sickbeard.MY_FULLNAME) + sickbeard.PROG_DIR = os.path.dirname(sickbeard.MY_FULLNAME) + sickbeard.DATA_DIR = sickbeard.PROG_DIR + sickbeard.MY_ARGS = sys.argv[1:] sickbeard.CREATEPID = False - try: - locale.setlocale(locale.LC_ALL, "") - except (locale.Error, IOError): - pass - sickbeard.SYS_ENCODING = locale.getpreferredencoding() - - # for OSes that are poorly configured I'll just force UTF-8 - if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII'): - sickbeard.SYS_ENCODING = 'UTF-8' - - sickbeard.CONFIG_FILE = os.path.join(sickbeard.PROG_DIR, "config.ini") - - # need console logging for SickBeard.py and SickBeard-console.exe - consoleLogging = (not hasattr(sys, "frozen")) or (sickbeard.MY_NAME.lower().find('-console') > 0) - - # rename the main thread - threading.currentThread().name = "MAIN" - - try: - opts, args = getopt.getopt(sys.argv[1:], "qfdp::", ['quiet', 'forceupdate', 'daemon', 'port=', 'tvbinz', 'pidfile=']) - except getopt.GetoptError: - print "Available options: --quiet, --forceupdate, --port, --daemon --pidfile" - sys.exit() - - forceUpdate = False + sickbeard.SYS_ENCODING = None + + try: + locale.setlocale(locale.LC_ALL, "") + sickbeard.SYS_ENCODING = locale.getpreferredencoding() + except (locale.Error, IOError): + pass + + # for OSes that are poorly configured I'll just force UTF-8 + if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): + sickbeard.SYS_ENCODING = 'UTF-8' + + # need console logging for SickBeard.py and SickBeard-console.exe + consoleLogging = (not hasattr(sys, "frozen")) or (sickbeard.MY_NAME.lower().find('-console') > 0) + + # rename the main thread + threading.currentThread().name = "MAIN" + + try: + opts, args = getopt.getopt(sys.argv[1:], "qfdp::", ['quiet', 'forceupdate', 'daemon', 'port=', 'pidfile=', 'nolaunch', 'config=', 'datadir=']) #@UnusedVariable + except getopt.GetoptError: + print "Available options: --quiet, --forceupdate, --port, --daemon, --pidfile, --config, --datadir" + sys.exit() + + forceUpdate = False forcedPort = None - + noLaunch = False + for o, a in opts: - # for now we'll just silence the logging + # for now we'll just silence the logging if o in ('-q', '--quiet'): - consoleLogging = False - # for now we'll just silence the logging - if o in ('--tvbinz'): - sickbeard.SHOW_TVBINZ = True - + consoleLogging = False + # should we update right away? - if o in ('-f', '--forceupdate'): + if o in ('-f', '--forceupdate'): forceUpdate = True - # use a different port - if o in ('-p', '--port'): - forcedPort = int(a) + # should we update right away? + if o in ('--nolaunch',): + noLaunch = True - # Run as a daemon + # use a different port + if o in ('-p', '--port'): + forcedPort = int(a) + + # Run as a daemon if o in ('-d', '--daemon'): - if sys.platform == 'win32': - print "Daemonize not supported under Windows, starting normally" - else: - consoleLogging = False - sickbeard.DAEMON = True - + if sys.platform == 'win32': + print "Daemonize not supported under Windows, starting normally" + else: + consoleLogging = False + sickbeard.DAEMON = True + + # config file + if o in ('--config',): + sickbeard.CONFIG_FILE = os.path.abspath(a) + + # datadir + if o in ('--datadir',): + sickbeard.DATA_DIR = os.path.abspath(a) + # write a pidfile if requested - if o in ('--pidfile'): + if o in ('--pidfile',): sickbeard.PIDFILE = str(a) # if the pidfile already exists, sickbeard may still be running, so exit @@ -176,96 +183,120 @@ def main(): raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) else: logger.log(u"Not running in daemon mode. PID file creation disabled.") - - if consoleLogging: - print "Starting up Sick Beard "+SICKBEARD_VERSION+" from " + sickbeard.CONFIG_FILE - - # load the config and publish it to the sickbeard package - if not os.path.isfile(sickbeard.CONFIG_FILE): - logger.log(u"Unable to find config.ini, all settings will be default", logger.ERROR) - - sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) - - # initialize the config and our threads - sickbeard.initialize(consoleLogging=consoleLogging) - - sickbeard.showList = [] - - if sickbeard.DAEMON: - daemonize() - # use this pid for everything - sickbeard.PID = os.getpid() - - if forcedPort: - logger.log(u"Forcing web server to port "+str(forcedPort)) - startPort = forcedPort - else: - startPort = sickbeard.WEB_PORT - - logger.log(u"Starting Sick Beard on http://localhost:"+str(startPort)) - - if sickbeard.WEB_LOG: - log_dir = sickbeard.LOG_DIR - else: - log_dir = None - - # sickbeard.WEB_HOST is available as a configuration value in various - # places but is not configurable. It is supported here for historic - # reasons. - if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0': - webhost = sickbeard.WEB_HOST - else: - if sickbeard.WEB_IPV6: - webhost = '::' - else: - webhost = '0.0.0.0' - - try: - initWebServer({ - 'port': startPort, - 'host': webhost, - 'data_root': os.path.join(sickbeard.PROG_DIR, 'data'), - 'web_root': sickbeard.WEB_ROOT, - 'log_dir': log_dir, - 'username': sickbeard.WEB_USERNAME, - 'password': sickbeard.WEB_PASSWORD, - }) - except IOError: - logger.log(u"Unable to start web server, is something else running on port %d?" % startPort, logger.ERROR) - if sickbeard.LAUNCH_BROWSER: - logger.log(u"Launching browser and exiting", logger.ERROR) - sickbeard.launchBrowser(startPort) - sys.exit() - - # build from the DB to start with - logger.log(u"Loading initial show list") - loadShowsFromDB() - - # fire up all our threads - sickbeard.start() - - # launch browser if we're supposed to - if sickbeard.LAUNCH_BROWSER: - sickbeard.launchBrowser(startPort) - - # start an update if we're supposed to - if forceUpdate: - sickbeard.showUpdateScheduler.action.run(force=True) - - # stay alive while my threads do the work - while (True): - - if sickbeard.invoked_command: - logger.log(u"Executing invoked command: "+repr(sickbeard.invoked_command)) - sickbeard.invoked_command() - sickbeard.invoked_command = None - - time.sleep(1) - - return - -if __name__ == "__main__": - if sys.hexversion >= 0x020600F0: - freeze_support() - main() + # if they don't specify a config file then put it in the data dir + if not sickbeard.CONFIG_FILE: + sickbeard.CONFIG_FILE = os.path.join(sickbeard.DATA_DIR, "config.ini") + + # make sure that we can create the data dir + if not os.access(sickbeard.DATA_DIR, os.F_OK): + try: + os.makedirs(sickbeard.DATA_DIR, 0744) + except os.error, e: + raise SystemExit("Unable to create datadir '" + sickbeard.DATA_DIR + "'") + + # make sure we can write to the data dir + if not os.access(sickbeard.DATA_DIR, os.W_OK): + raise SystemExit("Data dir must be writeable '" + sickbeard.DATA_DIR + "'") + + # make sure we can write to the config file + if not os.access(sickbeard.CONFIG_FILE, os.W_OK): + if os.path.isfile(sickbeard.CONFIG_FILE): + raise SystemExit("Config file '" + sickbeard.CONFIG_FILE + "' must be writeable") + elif not os.access(os.path.dirname(sickbeard.CONFIG_FILE), os.W_OK): + raise SystemExit("Config file root dir '" + os.path.dirname(sickbeard.CONFIG_FILE) + "' must be writeable") + + os.chdir(sickbeard.DATA_DIR) + + if consoleLogging: + print "Starting up Sick Beard "+SICKBEARD_VERSION+" from " + sickbeard.CONFIG_FILE + + # load the config and publish it to the sickbeard package + if not os.path.isfile(sickbeard.CONFIG_FILE): + logger.log(u"Unable to find " + sickbeard.CONFIG_FILE + " , all settings will be default", logger.WARNING) + + sickbeard.CFG = ConfigObj(sickbeard.CONFIG_FILE) + + # initialize the config and our threads + sickbeard.initialize(consoleLogging=consoleLogging) + + sickbeard.showList = [] + + if sickbeard.DAEMON: + daemonize() + + # use this pid for everything + sickbeard.PID = os.getpid() + + if forcedPort: + logger.log(u"Forcing web server to port "+str(forcedPort)) + startPort = forcedPort + else: + startPort = sickbeard.WEB_PORT + + logger.log(u"Starting Sick Beard on http://localhost:"+str(startPort)) + + if sickbeard.WEB_LOG: + log_dir = sickbeard.LOG_DIR + else: + log_dir = None + + # sickbeard.WEB_HOST is available as a configuration value in various + # places but is not configurable. It is supported here for historic + # reasons. + if sickbeard.WEB_HOST and sickbeard.WEB_HOST != '0.0.0.0': + webhost = sickbeard.WEB_HOST + else: + if sickbeard.WEB_IPV6: + webhost = '::' + else: + webhost = '0.0.0.0' + + try: + initWebServer({ + 'port': startPort, + 'host': webhost, + 'data_root': os.path.join(sickbeard.PROG_DIR, 'data'), + 'web_root': sickbeard.WEB_ROOT, + 'log_dir': log_dir, + 'username': sickbeard.WEB_USERNAME, + 'password': sickbeard.WEB_PASSWORD, + }) + except IOError: + logger.log(u"Unable to start web server, is something else running on port %d?" % startPort, logger.ERROR) + if sickbeard.LAUNCH_BROWSER: + logger.log(u"Launching browser and exiting", logger.ERROR) + sickbeard.launchBrowser(startPort) + sys.exit() + + # build from the DB to start with + logger.log(u"Loading initial show list") + loadShowsFromDB() + + # fire up all our threads + sickbeard.start() + + # launch browser if we're supposed to + if sickbeard.LAUNCH_BROWSER and not noLaunch: + sickbeard.launchBrowser(startPort) + + # start an update if we're supposed to + if forceUpdate: + sickbeard.showUpdateScheduler.action.run(force=True) #@UndefinedVariable + + # stay alive while my threads do the work + while (True): + + if sickbeard.invoked_command: + logger.log(u"Executing invoked command: "+repr(sickbeard.invoked_command)) + sickbeard.invoked_command() + sickbeard.invoked_command = None + + time.sleep(1) + + return + +if __name__ == "__main__": + if sys.hexversion >= 0x020600F0: + freeze_support() + main() diff --git a/data/css/config.css b/data/css/config.css index f52f8a54c70c4ef51e32d64bf9110be7ad261276..45dc728101f8db1cd6c7668f6b478806b5a84b16 100644 --- a/data/css/config.css +++ b/data/css/config.css @@ -12,6 +12,7 @@ #config-content{display:block;width:835px;text-align:left;clear:both;background:#fff;margin:0 auto;padding:0 0 40px;} #config-components{float:left;width:auto;} #config-components-border{float:left;width:auto;border-top:1px solid #999;padding:5px 0;} +#config .title-group{border-bottom:1px dotted #666;position:relative;padding:25px 15px 25px;} #config .component-group{border-bottom:1px dotted #666;position:relative;padding:15px 15px 25px;} #config .component-group-desc{float:left;width:235px;} #config .component-group-desc h3{font-size:1.5em;} @@ -57,4 +58,11 @@ .infoTableHeader, .infoTableCell {padding: 5px;} .infoTableHeader{font-weight:700;} -#config div.testNotification {border: 1px dotted #CCCCCC; padding: 5px; margin-bottom: 10px; line-height:20px;} \ No newline at end of file +#config div.testNotification {border: 1px dotted #CCCCCC; padding: 5px; margin-bottom: 10px; line-height:20px;} + +.config_message { + width: 100%; + text-align: center; + font-size: 1.3em; + background: #ffecac; +} \ No newline at end of file diff --git a/data/css/default.css b/data/css/default.css index 3435ae355a20a9211f623f4876ac3bf507b38c8c..f77ce6b0d8c779535bb647459586ebbba516c59c 100644 --- a/data/css/default.css +++ b/data/css/default.css @@ -2,7 +2,7 @@ img { border: 0; vertical-align: middle;} body { -background-color:#fff; +background-color:#F5F1E4; color:#000; font-family:'Verdana', 'Helvetica', 'Sans-serif', 'sans'; font-size:12px; @@ -36,6 +36,7 @@ display:inline; /* these are for incl_top.tmpl */ #header { +background-color:#fff; padding: 5px 0; z-index:2; } @@ -130,6 +131,7 @@ background-color:#F5F1E4; border-top:1px solid #b3b3b3; color:#4e4e4e; line-height: 1.4em; +font-size: 1em; } .sickbeardTable { @@ -349,4 +351,14 @@ div#addShowPortal button .buttontext { position: relative; display: block; padd .optionWrapper div.selectChoices { float: left; width: 175px; margin-left: 25px; } .optionWrapper br { clear: both; } -a.whitelink { color: white; } \ No newline at end of file +a.whitelink { color: white; } + +/* for displayShow notice */ +#show_message { + padding: 5px 8px; + background: #ffd575; + color: #333333; + text-shadow: 1px 1px 0 rgba(255,255,255,0.3); + font-size: 1em; +} +div.ui-pnotify { min-width: 340px; max-width: 550px; width: auto !important;} \ No newline at end of file diff --git a/data/images/loading16_dddddd.gif b/data/images/loading16_dddddd.gif new file mode 100644 index 0000000000000000000000000000000000000000..190582b822618e226f1beed6cee1135fcfaf5984 Binary files /dev/null and b/data/images/loading16_dddddd.gif differ diff --git a/data/images/loading_posters.gif b/data/images/loading_posters.gif deleted file mode 100644 index 075aeba0f2d57e8080fb2e873e8ced4e601a3459..0000000000000000000000000000000000000000 Binary files a/data/images/loading_posters.gif and /dev/null differ diff --git a/data/images/providers/sick_beard_index.gif b/data/images/providers/sick_beard_index.gif new file mode 100644 index 0000000000000000000000000000000000000000..73f8ae5c2e25e640e31286578607e692946a32c4 Binary files /dev/null and b/data/images/providers/sick_beard_index.gif differ diff --git a/data/interfaces/default/config.tmpl b/data/interfaces/default/config.tmpl index a070c2fe3964a13a6526477e5d040d014b31aed2..cbf8318e21d5bb4ef4259412650e5d8598f60a64 100644 --- a/data/interfaces/default/config.tmpl +++ b/data/interfaces/default/config.tmpl @@ -1,4 +1,5 @@ #import sickbeard +#from sickbeard import db #import os.path #set global $title="Configuration" @@ -11,6 +12,7 @@ <table class="infoTable" cellspacing="1" border="0" cellpadding="0"> <tr><td class="infoTableHeader">SB Version: </td><td class="infoTableCell">alpha ($sickbeard.version.SICKBEARD_VERSION) <!-- – build.date //--></td></tr> <tr><td class="infoTableHeader">SB Config file: </td><td class="infoTableCell">$sickbeard.CONFIG_FILE</td></tr> + <tr><td class="infoTableHeader">SB Database file: </td><td class="infoTableCell">$db.dbFilename()</td></tr> <tr><td class="infoTableHeader">SB Cache Dir: </td><td class="infoTableCell">$sickbeard.CACHE_DIR</td></tr> <tr><td class="infoTableHeader">SB Arguments: </td><td class="infoTableCell">$sickbeard.MY_ARGS</td></tr> <tr><td class="infoTableHeader">SB Web Root: </td><td class="infoTableCell">$sickbeard.WEB_ROOT</td></tr> diff --git a/data/interfaces/default/config_general.tmpl b/data/interfaces/default/config_general.tmpl index 2cbcd3975a4e520779b08ab7cc85ec3e688a1ffe..ee7bc51e4e2c04a68142c97402657964b7065083 100644 --- a/data/interfaces/default/config_general.tmpl +++ b/data/interfaces/default/config_general.tmpl @@ -12,11 +12,13 @@ #set global $topmenu="config"# #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") +<script type="text/javascript" src="$sbRoot/js/config.js"></script> + <div id="config"> <div id="config-content"> -<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.PROG_DIR</span> "</h5> +<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.DATA_DIR</span> "</h5> -<form action="saveGeneral" method="post"> +<form id="configForm" action="saveGeneral" method="post"> <div id="config-components"> @@ -56,7 +58,7 @@ </label> </div> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </fieldset> </div><!-- /component-group1 //--> @@ -119,11 +121,11 @@ </label> </div> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </fieldset> </div><!-- /component-group3 //--> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /config-components --> </form> diff --git a/data/interfaces/default/config_notifications.tmpl b/data/interfaces/default/config_notifications.tmpl index d8a8596606b42b4e51140305456ed02f5ab18ce2..963be414baa0d4d3f26eab2fb0fcb071a371b4e5 100644 --- a/data/interfaces/default/config_notifications.tmpl +++ b/data/interfaces/default/config_notifications.tmpl @@ -13,7 +13,7 @@ <div id="config"> <div id="config-content"> -<form id="form" action="saveNotifications" method="post"> +<form id="configForm" action="saveNotifications" method="post"> <div id="config-components"> @@ -105,7 +105,7 @@ <div class="testNotification" id="testXBMC-result">Click below to test.</div> <input type="button" value="Test XBMC" id="testXBMC" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /enabler_xbmc //--> @@ -199,7 +199,7 @@ <div class="testNotification" id="testPLEX-result">Click below to test.</div> <input type="button" value="Test Plex Media Server" id="testPLEX" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /enabler_plex --> @@ -265,9 +265,9 @@ </label> </div> - <div class="testNotification" id="testGrowl-result">Click below to test.</div> - <input type="button" value="Test Growl" id="testGrowl" /> - <input type="submit" value="Save Changes" /> + <div class="testNotification" id="testGrowl-result">Click below to register and test Growl, this is required for Growl notifications to work.</div> + <input type="button" value="Register Growl" id="testGrowl" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_growl //--> @@ -342,7 +342,7 @@ <div class="testNotification" id="testTwitter-result">Click below to test.</div> <input type="button" value="Test Twitter" id="testTwitter" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_twitter //--> @@ -412,7 +412,7 @@ <div class="testNotification" id="testProwl-result">Click below to test.</div> <input type="button" value="Test Prowl" id="testProwl" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_prowl //--> @@ -476,7 +476,7 @@ <div class="testNotification" id="testNotifo-result">Click below to test.</div> <input type="button" value="Test Notifo" id="testNotifo" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_notifo //--> @@ -519,7 +519,7 @@ <div class="testNotification" id="testLibnotify-result">Click below to test.</div> <input type="button" value="Test Libnotify" id="testLibnotify" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_libnotify //--> @@ -589,12 +589,12 @@ <div class="testNotification" id="testNMJ-result">Click below to test.</div> <input type="button" value="Test NMJ" id="testNMJ" /> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </div><!-- /content_use_nmj //--> </fieldset> </div><!-- /component-group //--> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /config-components //--> diff --git a/data/interfaces/default/config_postProcessing.tmpl b/data/interfaces/default/config_postProcessing.tmpl index eeb2434ab3ee76570f9021755d3e4c93275f6b4b..d8d0e211182ea84c5d25df1352a1fd604ee896ae 100644 --- a/data/interfaces/default/config_postProcessing.tmpl +++ b/data/interfaces/default/config_postProcessing.tmpl @@ -13,12 +13,13 @@ #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") <script type="text/javascript" src="$sbRoot/js/configPostProcessing.js"></script> +<script type="text/javascript" src="$sbRoot/js/config.js"></script> <div id="config"> <div id="config-content"> -<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.PROG_DIR</span> "</h5> +<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.DATA_DIR</span> "</h5> -<form action="savePostProcessing" method="post"> +<form id="configForm" action="savePostProcessing" method="post"> <div id="config-components"> @@ -91,7 +92,7 @@ </div> <div class="clearfix"></div> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </fieldset> </div><!-- /component-group3 //--> @@ -159,7 +160,7 @@ </label> </div> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </fieldset> </div><!-- /component-group2 //--> @@ -287,11 +288,11 @@ </label> </div> - <input type="submit" value="Save Changes" /> + <input type="submit" class="config_submitter" value="Save Changes" /> </fieldset> </div><!-- /component-group4 //--> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /config-components --> </form> diff --git a/data/interfaces/default/config_providers.tmpl b/data/interfaces/default/config_providers.tmpl index ff951fe05ab3e7091c264b67a973cb73d0b6c1ee..5054118aa2a915b7527272784c500364b9210034 100644 --- a/data/interfaces/default/config_providers.tmpl +++ b/data/interfaces/default/config_providers.tmpl @@ -10,6 +10,7 @@ #include $os.path.join($sickbeard.PROG_DIR, "data/interfaces/default/inc_top.tmpl") <script type="text/javascript" src="$sbRoot/js/configProviders.js"></script> +<script type="text/javascript" src="$sbRoot/js/config.js"></script> <script type="text/javascript" charset="utf-8"> <!-- \$(document).ready(function(){ @@ -25,7 +26,7 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <div id="config"> <div id="config-content"> -<form action="saveProviders" method="post"> +<form id="configForm" action="saveProviders" method="post"> <div id="config-components"> @@ -43,9 +44,6 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <fieldset class="component-group-list"> <ul id="provider_order_list"> #for $curProvider in $sickbeard.providers.sortedProviderList(): - #if $curProvider.getID() == 'tvbinz' and not $sickbeard.SHOW_TVBINZ - #continue - #end if #if $curProvider.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: #continue #elif $curProvider.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: @@ -62,14 +60,14 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; #end for </ul> <input type="hidden" name="provider_order" id="provider_order" value="<%=" ".join([x.getID()+':'+str(int(x.isEnabled())) for x in sickbeard.providers.sortedProviderList()])%>"/> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </fieldset> </div><!-- /component-group1 //--> <div id="core-component-group2" class="component-group clearfix"> <div class="component-group-desc"> - <h3>Configure Providers</h3> + <h3>Configure Built-In<br />Providers</h3> <p>Check with provider's website on how to obtain an API key if needed.</p> </div> @@ -79,11 +77,8 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <span class="component-title jumbo">Configure Provider:</span> <span class="component-desc"> #set $provider_config_list = [] - #for $cur_provider in ("nzbs_org", "nzbs_r_us", "newzbin", "nzbmatrix", "tvbinz", "tvtorrents"): + #for $cur_provider in ("nzbs_org", "nzbs_r_us", "newzbin", "nzbmatrix", "tvtorrents"): #set $cur_provider_obj = $sickbeard.providers.getProviderClass($cur_provider) - #if $cur_provider_obj.getID() == 'tvbinz' and not $sickbeard.SHOW_TVBINZ - #continue - #end if #if $cur_provider_obj.providerType == $GenericProvider.NZB and not $sickbeard.USE_NZBS: #continue #elif $cur_provider_obj.providerType == $GenericProvider.TORRENT and not $sickbeard.USE_TORRENTS: @@ -94,7 +89,7 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; #if $provider_config_list: <select id="editAProvider"> - #for $cur_provider in $provider_config_list: + #for $cur_provider in $provider_config_list + [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth]: <option value="$cur_provider.getID()">$cur_provider.name</option> #end for </select> @@ -107,28 +102,23 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <!-- start div for editing providers //--> -#if $sickbeard.SHOW_TVBINZ: -<div class="providerDiv" id="tvbinzDiv"> - <div class="field-pair"> - <label class="clearfix"> - <span class="component-title">TVBinz Cookie UID</span> - <input class="component-desc" type="text" name="tvbinz_uid" value="$sickbeard.TVBINZ_UID" size="10" /> - </label> - </div> +#for $curNewznabProvider in [$curProvider for $curProvider in $sickbeard.newznabProviderList if $curProvider.default and $curProvider.needs_auth]: +<div class="providerDiv" id="${curNewznabProvider.getID()}Div"> <div class="field-pair"> <label class="clearfix"> - <span class="component-title">TVBinz Hash</span> - <input class="component-desc" type="text" name="tvbinz_hash" value="$sickbeard.TVBINZ_HASH" size="40" /> + <span class="component-title">$curNewznabProvider.name URL</span> + <input class="component-desc" type="text" id="${curNewznabProvider.getID()}_url" value="$curNewznabProvider.url" size="40" disabled/> </label> </div> <div class="field-pair"> <label class="clearfix"> - <span class="component-title">TVBinz Auth String</span> - <input class="component-desc" type="text" name="tvbinz_auth" value="$sickbeard.TVBINZ_AUTH" size="40" /> + <span class="component-title">$curNewznabProvider.name API Key</span> + <input class="component-desc newznab_key" type="text" id="${curNewznabProvider.getID()}_hash" value="$curNewznabProvider.key" size="40" /> </label> </div> </div> -#end if +#end for + <div class="providerDiv" id="nzbs_orgDiv"> <div class="field-pair"> @@ -207,7 +197,7 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <!-- end div for editing providers --> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </fieldset> </div><!-- /component-group2 //--> @@ -215,9 +205,9 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; <div id="core-component-group3" class="component-group clearfix"> <div class="component-group-desc"> - <h3>Newznab Providers</h3> - <p>Add and setup newznab providers.</p> - <p>NZB.su has already been added as an example and can not be deleted.</p> + <h3>Configure Custom<br />Newznab Providers</h3> + <p>Add and setup custom Newznab providers.</p> + <p>Some built-in Newznab providers are already available above.</p> </div> <fieldset class="component-group-list"> @@ -268,7 +258,7 @@ var show_nzb_providers = #if $sickbeard.USE_NZBS then "true" else "false"#; </div><!-- /component-group3 //--> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /config-components //--> diff --git a/data/interfaces/default/config_search.tmpl b/data/interfaces/default/config_search.tmpl index 1d7b64f42d4df46bc0189d96dbfa039837b7cb8a..600b29f1440e9154accff420629b793f4cb9d4fa 100644 --- a/data/interfaces/default/config_search.tmpl +++ b/data/interfaces/default/config_search.tmpl @@ -13,8 +13,9 @@ <div id="config"> <div id="config-content"> -<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.PROG_DIR</span> "</h5> -<form action="saveSearch" method="post"> +<h5>All non-absolute folder locations are relative to " <span class="path">$sickbeard.DATA_DIR</span> "</h5> + +<form id="configForm" action="saveSearch" method="post"> <div id="config-components"> @@ -57,7 +58,7 @@ </div> <div class="clearfix"></div> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </fieldset> </div><!-- /component-group1 //--> @@ -213,13 +214,17 @@ </div> <div class="clearfix"></div> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /content_use_nzbs //--> </fieldset> </div><!-- /component-group2 //--> + <div class="title-group clearfix" id="no-torrents"> + <div class="ui-corner-all config_message">Note: Sick Beard works better with Usenet than with Torrents, <a href="http://www.sickbeard.com/usenet.html" target="_blank">here's why</a>.</div> + </div> + <div id="core-component-group3" class="component-group clearfix"> <div class="component-group-desc"> @@ -250,13 +255,13 @@ </div> <div class="clearfix"></div> - <input type="submit" value="Save Changes" /><br/> + <input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /content_use_torrents //--> </fieldset> </div><!-- /component-group3 //--> - <br/><input type="submit" value="Save Changes" /><br/> + <br/><input type="submit" class="config_submitter" value="Save Changes" /><br/> </div><!-- /config-components //--> diff --git a/data/interfaces/default/displayShow.tmpl b/data/interfaces/default/displayShow.tmpl index d1e0bd3900c351543ee245817d2808e96988509a..4cfed8cdb63f41d8a1831f8f934c4e200cabb7a0 100644 --- a/data/interfaces/default/displayShow.tmpl +++ b/data/interfaces/default/displayShow.tmpl @@ -11,6 +11,7 @@ <script type="text/javascript" src="$sbRoot/js/jquery.bookmarkscroll.js"></script> + <div class="h2footer align-right"> #if (len($seasonResults) > 14): <select id="seasonJump"> @@ -34,6 +35,10 @@ #end if </div><br/> +#if $show_message: + <div id="show_message" class="ui-corner-all">$show_message</div><br /> +#end if + <input type="hidden" id="sbRoot" value="$sbRoot" /> <script type="text/javascript" src="$sbRoot/js/displayShow.js"></script> @@ -150,10 +155,10 @@ $epLoc #end if </td> - <td>$statusStrings[int($epResult["status"])]</td> + <td class="status_column">$statusStrings[int($epResult["status"])]</td> <td align="center"> - #if int($epResult["season"]) != 0: - <a href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search32.png" height="16" alt="search" title="Manual Search" /></a> + #if int($epResult["season"]) != 0: + <a class="epSearch" href="searchEpisode?show=$show.tvdbid&season=$epResult["season"]&episode=$epResult["episode"]"><img src="$sbRoot/images/search32.png" height="16" alt="search" title="Manual Search" /></a> #end if </td> </tr> diff --git a/data/interfaces/default/inc_top.tmpl b/data/interfaces/default/inc_top.tmpl index 2ebccfb271aa64f25f8f27b70718b842c137c7dd..66e13afc5704623cc3c2efd1ae2ff19652dd967c 100644 --- a/data/interfaces/default/inc_top.tmpl +++ b/data/interfaces/default/inc_top.tmpl @@ -73,6 +73,7 @@ table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/imag <script type="text/javascript" src="$sbRoot/js/tools.tooltip-1.2.5.min.js"></script> <script type="text/javascript" src="$sbRoot/js/jquery.pnotify-1.0.1.min.js"></script> <script type="text/javascript" src="$sbRoot/js/jquery.expand-1.3.8.js"></script> + <script type="text/javascript" src="$sbRoot/js/jquery.form-2.69.js"></script> <script type="text/javascript" charset="utf-8"> <!-- @@ -91,6 +92,7 @@ table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/imag </script> <script type="text/javascript" src="$sbRoot/js/jquery.scrolltopcontrol-1.1.js"></script> <script type="text/javascript" src="$sbRoot/js/browser.js"></script> + <script type="text/javascript" src="$sbRoot/js/ajaxNotifications.js"></script> <script type="text/javascript"> <!-- @@ -139,22 +141,6 @@ table.tablesorter thead tr .headerSortDown { background-image: url("$sbRoot/imag \$.pnotify.defaults.pnotify_history = false; \$.pnotify.defaults.pnotify_delay = 4000; - #for $curMessage in $flash.messages(): - \$.pnotify({ - pnotify_title: decodeURIComponent('$urllib.quote($curMessage[0].encode('UTF-8'))'), - pnotify_text: decodeURIComponent('$urllib.quote($curMessage[1].encode('UTF-8'))') - }); - #end for - - #for $curError in $flash.errors(): - \$.pnotify({ - pnotify_type: 'error', - pnotify_hide: false, - pnotify_title: decodeURIComponent('$urllib.quote($curError[0].encode('UTF-8'))'), - pnotify_text: decodeURIComponent('$urllib.quote($curError[1].encode('UTF-8'))') - }); - #end for - }); //--> </script> diff --git a/data/js/ajaxNotifications.js b/data/js/ajaxNotifications.js new file mode 100644 index 0000000000000000000000000000000000000000..3082035a908d9603ed0f97df0dfd956f4b5fb04f --- /dev/null +++ b/data/js/ajaxNotifications.js @@ -0,0 +1,22 @@ +var message_url = sbRoot + '/ui/get_messages'; + +function check_notifications() { + $.getJSON(message_url, function(data){ + $.each(data, function(name,data){ + $.pnotify({ + pnotify_type: data.type, + pnotify_hide: data.type == 'notice', + pnotify_title: data.title, + pnotify_text: data.message + }); + }); + }); + + setTimeout(check_notifications, 3000) +} + +$(document).ready(function(){ + + check_notifications(); + +}); \ No newline at end of file diff --git a/data/js/config.js b/data/js/config.js index 37d3a24c1344320e075ae0ad44238afdaf363d32..c5a31e114ae6cca3297e23ae3e3b75bf5cab8d01 100644 --- a/data/js/config.js +++ b/data/js/config.js @@ -10,4 +10,27 @@ $(document).ready(function(){ else $('#content_'+$(this).attr('id')).hide(); }); -}); \ No newline at end of file + + // bind 'myForm' and provide a simple callback function + $('#configForm').ajaxForm({ + beforeSubmit: function(){ + $('.config_submitter').each(function(){ + $(this).attr("disabled", "disabled"); + $(this).after('<span><img src="'+sbRoot+'/images/loading16.gif"> Saving...</span>'); + $(this).hide(); + }); + }, + success: function(){ + setTimeout('config_success()', 2000) + } + }); + +}); + +function config_success(){ + $('.config_submitter').each(function(){ + $(this).removeAttr("disabled"); + $(this).next().remove(); + $(this).show(); + }); +} \ No newline at end of file diff --git a/data/js/configProviders.js b/data/js/configProviders.js index 137b8ce0b402b424856196f2071b349f977a9d8b..3a6d3630a6ce3ffa1fda40ba1cf5045c598980e6 100644 --- a/data/js/configProviders.js +++ b/data/js/configProviders.js @@ -21,8 +21,11 @@ $(document).ready(function(){ var newData = [isDefault, [name, url, key]]; newznabProviders[id] = newData; - $('#editANewznabProvider').addOption(id, name); - $(this).populateNewznabSection(); + if (!isDefault) + { + $('#editANewznabProvider').addOption(id, name); + $(this).populateNewznabSection(); + } if ($('#provider_order_list > #'+id).length == 0 && showProvider != false) { var toAdd = '<li class="ui-state-default" id="'+id+'"> <input type="checkbox" id="enable_'+id+'" class="provider_enabler" CHECKED> <a href="'+url+'" class="imgLink" target="_new"><img src="'+sbRoot+'/images/providers/newznab.gif" alt="'+name+'" width="16" height="16"></a> '+name+'</li>' @@ -120,10 +123,22 @@ $(document).ready(function(){ } var newznabProviders = new Array(); + + $('.newznab_key').change(function(){ + + var provider_id = $(this).attr('id'); + provider_id = provider_id.substring(0, provider_id.length-'_hash'.length); + + var url = $('#'+provider_id+'_url').val(); + var key = $(this).val(); + + $(this).updateProvider(provider_id, url, key); + + }); $('#newznab_key').change(function(){ - var selectedProvider = $('#editANewznabProvider :selected').val(); + var selectedProvider = $('#editANewznabProvider :selected').val(); var url = $('#newznab_url').val(); var key = $('#newznab_key').val(); diff --git a/data/js/configSearch.js b/data/js/configSearch.js index 01d8f89bc8beb40a835b97a29c4f1278998deb26..6dba86d8a5826571e6374c428496f0bb8f9d6adb 100644 --- a/data/js/configSearch.js +++ b/data/js/configSearch.js @@ -1,5 +1,12 @@ $(document).ready(function(){ + function toggle_torrent_title(){ + if ($('#use_torrents').attr('checked')) + $('#no-torrents').show(); + else + $('#no-torrents').hide(); + } + $.fn.nzb_method_handler = function() { var selectedProvider = $('#nzb_method :selected').val(); @@ -24,4 +31,10 @@ $(document).ready(function(){ $(this).nzb_method_handler(); + $('#use_torrents').click(function(){ + toggle_torrent_title(); + }); + + toggle_torrent_title(); + }); diff --git a/data/js/displayShow.js b/data/js/displayShow.js index d5e0ab23670afd7d3b7dce40df9cd52b400dbcbf..471244d5f9fb540611314c072021ad11e5d32667 100644 --- a/data/js/displayShow.js +++ b/data/js/displayShow.js @@ -1,12 +1,43 @@ $(document).ready(function(){ + $('.epSearch').click(function(){ + var parent = $(this).parent(); + + // put the ajax spinner (for non white bg) placeholder while we wait + parent.empty(); + parent.append($("<img/>").attr({"src": sbRoot+"/images/loading16_dddddd.gif", "height": "16", "alt": "", "title": "loading"})); + + $.getJSON($(this).attr('href'), function(data){ + // if they failed then just put the red X + if (data.result == 'failure') { + img_name = 'no16.png'; + img_result = 'failed'; + + // if the snatch was successful then apply the corresponding class and fill in the row appropriately + } else { + img_name = 'yes16.png'; + img_result = 'success'; + // color the row + parent.parent().removeClass('skipped wanted qual good unaired').addClass('good'); + parent.siblings('.status_column').html(data.result); + } + + // put the corresponding image as the result for the the row + parent.empty(); + parent.append($("<img/>").attr({"src": sbRoot+"/images/"+img_name, "height": "16", "alt": img_result, "title": img_result})); + }); + + // fon't follow the link + return false; + }); + $('#seasonJump').change(function() { - var id = $(this).val(); - if (id && id != 'jump') { - $('html,body').animate({scrollTop: $(id).offset().top},'slow'); - location.hash = id; - } - $(this).val('jump'); + var id = $(this).val(); + if (id && id != 'jump') { + $('html,body').animate({scrollTop: $(id).offset().top},'slow'); + location.hash = id; + } + $(this).val('jump'); }); $("#prevShow").click(function(){ @@ -20,7 +51,6 @@ $(document).ready(function(){ }); $('#changeStatus').click(function(){ - var sbRoot = $('#sbRoot').val() var epArr = new Array() @@ -36,13 +66,11 @@ $(document).ready(function(){ return false url = sbRoot+'/home/setStatus?show='+$('#showID').attr('value')+'&eps='+epArr.join('|')+'&status='+$('#statusSelect').attr('value') - window.location.href = url }); $('.seasonCheck').click(function(){ - var seasCheck = this; var seasNo = $(seasCheck).attr('id'); @@ -50,8 +78,8 @@ $(document).ready(function(){ var epParts = $(this).attr('id').split('x') if (epParts[0] == seasNo) { - this.checked = seasCheck.checked - } + this.checked = seasCheck.checked + } }); }); diff --git a/data/js/jquery.form-2.69.js b/data/js/jquery.form-2.69.js new file mode 100644 index 0000000000000000000000000000000000000000..3ad71f891d3338cd39ba4b69b23d0c3d43bb3992 --- /dev/null +++ b/data/js/jquery.form-2.69.js @@ -0,0 +1,815 @@ +/*! + * jQuery Form Plugin + * version: 2.69 (06-APR-2011) + * @requires jQuery v1.3.2 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ +;(function($) { + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are intended to be exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').bind('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + if (typeof options == 'function') { + options = { success: options }; + } + + var action = this.attr('action'); + var url = (typeof action === 'string') ? $.trim(action) : ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + url = url || window.location.href || ''; + + options = $.extend(true, { + url: url, + success: $.ajaxSettings.success, + type: this[0].getAttribute('method') || 'GET', // IE7 massage (see issue 57) + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var n,v,a = this.formToArray(options.semantic); + if (options.data) { + options.extraData = options.data; + for (n in options.data) { + if(options.data[n] instanceof Array) { + for (var k in options.data[n]) { + a.push( { name: n, value: options.data[n][k] } ); + } + } + else { + v = options.data[n]; + v = $.isFunction(v) ? v() : v; // if value is fn, invoke it + a.push( { name: n, value: v } ); + } + } + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else { + options.data = q; // data is the query string for 'post' + } + + var $form = this, callbacks = []; + if (options.resetForm) { + callbacks.push(function() { $form.resetForm(); }); + } + if (options.clearForm) { + callbacks.push(function() { $form.clearForm(); }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + var fn = options.replaceTarget ? 'replaceWith' : 'html'; + $(options.target)[fn](data).each(oldSuccess, arguments); + }); + } + else if (options.success) { + callbacks.push(options.success); + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || options; // jQuery 1.4+ supports scope context + for (var i=0, max=callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + // are there files to upload? + var fileInputs = $('input:file', this).length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (fileInputs || options.iframe || multipart)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, fileUpload); + } + else { + fileUpload(); + } + } + else { + $.ajax(options); + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[name=submit],:input[id=submit]', form).length) { + // if there is an input with a name or id of 'submit' then we won't be + // able to invoke the submit fn on the form (at least not x-browser) + alert('Error: Form elements must not have name or id of "submit".'); + return; + } + + var s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + var id = 'jqFormIO' + (new Date().getTime()), fn = '_'+id; + var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ s.iframeSrc +'" />'); + var io = $io[0]; + + $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' }); + + var xhr = { // mock object + aborted: 0, + responseText: null, + responseXML: null, + status: 0, + statusText: 'n/a', + getAllResponseHeaders: function() {}, + getResponseHeader: function() {}, + setRequestHeader: function() {}, + abort: function() { + log('aborting upload...'); + var e = 'aborted'; + this.aborted = 1; + $io.attr('src', s.iframeSrc); // abort op in progress + xhr.error = e; + s.error && s.error.call(s.context, xhr, 'error', e); + g && $.event.trigger("ajaxError", [xhr, s, e]); + s.complete && s.complete.call(s.context, xhr, 'error'); + } + }; + + var g = s.global; + // trigger ajax global events so that activity/block indicators work like normal + if (g && ! $.active++) { + $.event.trigger("ajaxStart"); + } + if (g) { + $.event.trigger("ajaxSend", [xhr, s]); + } + + if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) { + if (s.global) { + $.active--; + } + return; + } + if (xhr.aborted) { + return; + } + + var timedOut = 0; + + // add submitting element to data if we know it + var sub = form.clk; + if (sub) { + var n = sub.name; + if (n && !sub.disabled) { + s.extraData = s.extraData || {}; + s.extraData[n] = sub.value; + if (sub.type == "image") { + s.extraData[n+'.x'] = form.clk_x; + s.extraData[n+'.y'] = form.clk_y; + } + } + } + + // take a breath so that pending repaints get some cpu time before the upload starts + function doSubmit() { + // make sure form attrs are set + var t = $form.attr('target'), a = $form.attr('action'); + + // update form attrs in IE friendly way + form.setAttribute('target',id); + if (form.getAttribute('method') != 'POST') { + form.setAttribute('method', 'POST'); + } + if (form.getAttribute('action') != s.url) { + form.setAttribute('action', s.url); + } + + // ie borks in some cases when setting encoding + if (! s.skipEncodingOverride) { + $form.attr({ + encoding: 'multipart/form-data', + enctype: 'multipart/form-data' + }); + } + + // support timout + if (s.timeout) { + setTimeout(function() { timedOut = true; cb(); }, s.timeout); + } + + // add "extra" data to form if provided in options + var extraInputs = []; + try { + if (s.extraData) { + for (var n in s.extraData) { + extraInputs.push( + $('<input type="hidden" name="'+n+'" value="'+s.extraData[n]+'" />') + .appendTo(form)[0]); + } + } + + // add iframe to doc and submit the form + $io.appendTo('body'); + io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false); + form.submit(); + } + finally { + // reset attrs and remove "extra" input elements + form.setAttribute('action',a); + if(t) { + form.setAttribute('target', t); + } else { + $form.removeAttr('target'); + } + $(extraInputs).remove(); + } + } + + if (s.forceSync) { + doSubmit(); + } + else { + setTimeout(doSubmit, 10); // this lets dom updates render + } + + var data, doc, domCheckCount = 50; + + function cb() { + if (xhr.aborted) { + return; + } + + var doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document; + if (!doc || doc.location.href == s.iframeSrc) { + // response not received yet + if (!timedOut) + return; + } + io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false); + + var ok = true; + try { + if (timedOut) { + throw 'timeout'; + } + + var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc); + log('isXml='+isXml); + if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) { + if (--domCheckCount) { + // in some browsers (Opera) the iframe DOM is not always traversable when + // the onload callback fires, so we loop a bit to accommodate + log('requeing onLoad callback, DOM not available'); + setTimeout(cb, 250); + return; + } + // let this fall through because server response could be an empty document + //log('Could not access iframe DOM after mutiple tries.'); + //throw 'DOMException: not available'; + } + + //log('response detected'); + xhr.responseText = doc.body ? doc.body.innerHTML : doc.documentElement ? doc.documentElement.innerHTML : null; + xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; + xhr.getResponseHeader = function(header){ + var headers = {'content-type': s.dataType}; + return headers[header]; + }; + + var scr = /(json|script)/.test(s.dataType); + if (scr || s.textarea) { + // see if user embedded response in textarea + var ta = doc.getElementsByTagName('textarea')[0]; + if (ta) { + xhr.responseText = ta.value; + } + else if (scr) { + // account for browsers injecting pre around json response + var pre = doc.getElementsByTagName('pre')[0]; + var b = doc.getElementsByTagName('body')[0]; + if (pre) { + xhr.responseText = pre.textContent; + } + else if (b) { + xhr.responseText = b.innerHTML; + } + } + } + else if (s.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) { + xhr.responseXML = toXml(xhr.responseText); + } + + data = httpData(xhr, s.dataType, s); + } + catch(e){ + log('error caught:',e); + ok = false; + xhr.error = e; + s.error && s.error.call(s.context, xhr, 'error', e); + g && $.event.trigger("ajaxError", [xhr, s, e]); + } + + if (xhr.aborted) { + log('upload aborted'); + ok = false; + } + + // ordering of these callbacks/triggers is odd, but that's how $.ajax does it + if (ok) { + s.success && s.success.call(s.context, data, 'success', xhr); + g && $.event.trigger("ajaxSuccess", [xhr, s]); + } + + g && $.event.trigger("ajaxComplete", [xhr, s]); + + if (g && ! --$.active) { + $.event.trigger("ajaxStop"); + } + + s.complete && s.complete.call(s.context, xhr, ok ? 'success' : 'error'); + + // clean up + setTimeout(function() { + $io.removeData('form-plugin-onload'); + $io.remove(); + xhr.responseXML = null; + }, 100); + } + + var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+) + if (window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + doc.loadXML(s); + } + else { + doc = (new DOMParser()).parseFromString(s, 'text/xml'); + } + return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null; + }; + var parseJSON = $.parseJSON || function(s) { + return window['eval']('(' + s + ')'); + }; + + var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4 + var ct = xhr.getResponseHeader('content-type') || '', + xml = type === 'xml' || !type && ct.indexOf('xml') >= 0, + data = xml ? xhr.responseXML : xhr.responseText; + + if (xml && data.documentElement.nodeName === 'parsererror') { + $.error && $.error('parsererror'); + } + if (s && s.dataFilter) { + data = s.dataFilter(data, type); + } + if (typeof data === 'string') { + if (type === 'json' || !type && ct.indexOf('json') >= 0) { + data = parseJSON(data); + } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) { + $.globalEval(data); + } + } + return data; + }; + } +}; + +/** + * ajaxForm() provides a mechanism for fully automating form submission. + * + * The advantages of using this method instead of ajaxSubmit() are: + * + * 1: This method will include coordinates for <input type="image" /> elements (if the element + * is used to submit the form). + * 2. This method will include the submit element's name/value data (for the element that was + * used to submit the form). + * 3. This method binds the submit() method to the form for you. + * + * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely + * passes the options argument along after properly binding events for submit elements and + * the form itself. + */ +$.fn.ajaxForm = function(options) { + // in jQuery 1.3+ we can fix mistakes with the ready state + if (this.length === 0) { + var o = { s: this.selector, c: this.context }; + if (!$.isReady && o.s) { + log('DOM not ready, queuing ajaxForm'); + $(function() { + $(o.s,o.c).ajaxForm(options); + }); + return this; + } + // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready() + log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)')); + return this; + } + + return this.ajaxFormUnbind().bind('submit.form-plugin', function(e) { + if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed + e.preventDefault(); + $(this).ajaxSubmit(options); + } + }).bind('click.form-plugin', function(e) { + var target = e.target; + var $el = $(target); + if (!($el.is(":submit,input:image"))) { + // is this a child element of the submit el? (ex: a span within a button) + var t = $el.closest(':submit'); + if (t.length == 0) { + return; + } + target = t[0]; + } + var form = this; + form.clk = target; + if (target.type == 'image') { + if (e.offsetX != undefined) { + form.clk_x = e.offsetX; + form.clk_y = e.offsetY; + } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin + var offset = $el.offset(); + form.clk_x = e.pageX - offset.left; + form.clk_y = e.pageY - offset.top; + } else { + form.clk_x = e.pageX - target.offsetLeft; + form.clk_y = e.pageY - target.offsetTop; + } + } + // clear form vars + setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100); + }); +}; + +// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm +$.fn.ajaxFormUnbind = function() { + return this.unbind('submit.form-plugin click.form-plugin'); +}; + +/** + * formToArray() gathers form element data into an array of objects that can + * be passed to any of the following ajax functions: $.get, $.post, or load. + * Each object in the array has both a 'name' and 'value' property. An example of + * an array for a simple login form might be: + * + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] + * + * It is this array that is passed to pre-submit callback functions provided to the + * ajaxSubmit() and ajaxForm() methods. + */ +$.fn.formToArray = function(semantic) { + var a = []; + if (this.length === 0) { + return a; + } + + var form = this[0]; + var els = semantic ? form.getElementsByTagName('*') : form.elements; + if (!els) { + return a; + } + + var i,j,n,v,el,max,jmax; + for(i=0, max=els.length; i < max; i++) { + el = els[i]; + n = el.name; + if (!n) { + continue; + } + + if (semantic && form.clk && el.type == "image") { + // handle image inputs on the fly when semantic == true + if(!el.disabled && form.clk == el) { + a.push({name: n, value: $(el).val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + continue; + } + + v = $.fieldValue(el, true); + if (v && v.constructor == Array) { + for(j=0, jmax=v.length; j < jmax; j++) { + a.push({name: n, value: v[j]}); + } + } + else if (v !== null && typeof v != 'undefined') { + a.push({name: n, value: v}); + } + } + + if (!semantic && form.clk) { + // input type=='image' are not found in elements array! handle it here + var $input = $(form.clk), input = $input[0]; + n = input.name; + if (n && !input.disabled && input.type == 'image') { + a.push({name: n, value: $input.val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + } + return a; +}; + +/** + * Serializes form data into a 'submittable' string. This method will return a string + * in the format: name1=value1&name2=value2 + */ +$.fn.formSerialize = function(semantic) { + //hand off to jQuery.param for proper encoding + return $.param(this.formToArray(semantic)); +}; + +/** + * Serializes all field elements in the jQuery object into a query string. + * This method will return a string in the format: name1=value1&name2=value2 + */ +$.fn.fieldSerialize = function(successful) { + var a = []; + this.each(function() { + var n = this.name; + if (!n) { + return; + } + var v = $.fieldValue(this, successful); + if (v && v.constructor == Array) { + for (var i=0,max=v.length; i < max; i++) { + a.push({name: n, value: v[i]}); + } + } + else if (v !== null && typeof v != 'undefined') { + a.push({name: this.name, value: v}); + } + }); + //hand off to jQuery.param for proper encoding + return $.param(a); +}; + +/** + * Returns the value(s) of the element in the matched set. For example, consider the following form: + * + * <form><fieldset> + * <input name="A" type="text" /> + * <input name="A" type="text" /> + * <input name="B" type="checkbox" value="B1" /> + * <input name="B" type="checkbox" value="B2"/> + * <input name="C" type="radio" value="C1" /> + * <input name="C" type="radio" value="C2" /> + * </fieldset></form> + * + * var v = $(':text').fieldValue(); + * // if no values are entered into the text inputs + * v == ['',''] + * // if values entered into the text inputs are 'foo' and 'bar' + * v == ['foo','bar'] + * + * var v = $(':checkbox').fieldValue(); + * // if neither checkbox is checked + * v === undefined + * // if both checkboxes are checked + * v == ['B1', 'B2'] + * + * var v = $(':radio').fieldValue(); + * // if neither radio is checked + * v === undefined + * // if first radio is checked + * v == ['C1'] + * + * The successful argument controls whether or not the field element must be 'successful' + * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls). + * The default value of the successful argument is true. If this value is false the value(s) + * for each element is returned. + * + * Note: This method *always* returns an array. If no valid value can be determined the + * array will be empty, otherwise it will contain one or more values. + */ +$.fn.fieldValue = function(successful) { + for (var val=[], i=0, max=this.length; i < max; i++) { + var el = this[i]; + var v = $.fieldValue(el, successful); + if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) { + continue; + } + v.constructor == Array ? $.merge(val, v) : val.push(v); + } + return val; +}; + +/** + * Returns the value of the field element. + */ +$.fieldValue = function(el, successful) { + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); + if (successful === undefined) { + successful = true; + } + + if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || + (t == 'checkbox' || t == 'radio') && !el.checked || + (t == 'submit' || t == 'image') && el.form && el.form.clk != el || + tag == 'select' && el.selectedIndex == -1)) { + return null; + } + + if (tag == 'select') { + var index = el.selectedIndex; + if (index < 0) { + return null; + } + var a = [], ops = el.options; + var one = (t == 'select-one'); + var max = (one ? index+1 : ops.length); + for(var i=(one ? index : 0); i < max; i++) { + var op = ops[i]; + if (op.selected) { + var v = op.value; + if (!v) { // extra pain for IE... + v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value; + } + if (one) { + return v; + } + a.push(v); + } + } + return a; + } + return $(el).val(); +}; + +/** + * Clears the form data. Takes the following actions on the form's input fields: + * - input text fields will have their 'value' property set to the empty string + * - select elements will have their 'selectedIndex' property set to -1 + * - checkbox and radio inputs will have their 'checked' property set to false + * - inputs of type submit, button, reset, and hidden will *not* be effected + * - button elements will *not* be effected + */ +$.fn.clearForm = function() { + return this.each(function() { + $('input,select,textarea', this).clearFields(); + }); +}; + +/** + * Clears the selected form elements. + */ +$.fn.clearFields = $.fn.clearInputs = function() { + return this.each(function() { + var t = this.type, tag = this.tagName.toLowerCase(); + if (t == 'text' || t == 'password' || tag == 'textarea') { + this.value = ''; + } + else if (t == 'checkbox' || t == 'radio') { + this.checked = false; + } + else if (tag == 'select') { + this.selectedIndex = -1; + } + }); +}; + +/** + * Resets the form data. Causes all form elements to be reset to their original value. + */ +$.fn.resetForm = function() { + return this.each(function() { + // guard against an input with the name of 'reset' + // note that IE reports the reset function as an 'object' + if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) { + this.reset(); + } + }); +}; + +/** + * Enables or disables any matching elements. + */ +$.fn.enable = function(b) { + if (b === undefined) { + b = true; + } + return this.each(function() { + this.disabled = !b; + }); +}; + +/** + * Checks/unchecks any matching checkboxes or radio buttons and + * selects/deselects and matching option elements. + */ +$.fn.selected = function(select) { + if (select === undefined) { + select = true; + } + return this.each(function() { + var t = this.type; + if (t == 'checkbox' || t == 'radio') { + this.checked = select; + } + else if (this.tagName.toLowerCase() == 'option') { + var $sel = $(this).parent('select'); + if (select && $sel[0] && $sel[0].type == 'select-one') { + // deselect all other options + $sel.find('option').selected(false); + } + this.selected = select; + } + }); +}; + +// helper fn for console logging +// set $.fn.ajaxSubmit.debug to true to enable debug logging +function log() { + if ($.fn.ajaxSubmit.debug) { + var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,''); + if (window.console && window.console.log) { + window.console.log(msg); + } + else if (window.opera && window.opera.postError) { + window.opera.postError(msg); + } + } +}; + +})(jQuery); diff --git a/init.solaris11 b/init.solaris11 new file mode 100644 index 0000000000000000000000000000000000000000..9c512a3dc1f3926e13be24ed65911a16984bd02e --- /dev/null +++ b/init.solaris11 @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1"> +<!-- + Assumes user=sickbeard group=other + Assumes /opt/sickbeard is installation directory + + See http://www.sun.com/bigadmin/content/selfheal/sdev_intro.jsp for more information + + To install (see http://docs.sun.com/app/docs/doc/819-2379/fgour?l=en&a=view for more information) + svccfg import sickbeard.smf + + To enable/disable + svcadm enable sickbeard + svcadm disable sickbeard + + To check if failures + svcs -xv + + To check logs + tail /var/svc/log/network-sickbeard\:default.log +--> + +<service_bundle type='manifest' name='sickbeard'> + + <service + name='network/sickbeard' + type='service' + version='1'> + + <create_default_instance enabled='false' /> + <single_instance /> + + <!-- + Only start in muti-user mode + --> + <dependency name='multi-user' + grouping='require_all' + restart_on='none' + type='service'> + <service_fmri value='svc:/milestone/multi-user' /> + </dependency> + + <!-- + Wait for network interfaces to be initialized. + --> + <dependency name='network' + grouping='require_all' + restart_on='none' + type='service'> + <service_fmri value='svc:/milestone/network:default'/> + </dependency> + + <!-- + Wait for all local filesystems to be mounted. + --> + <dependency name='filesystem-local' + grouping='require_all' + restart_on='none' + type='service'> + <service_fmri value='svc:/system/filesystem/local:default'/> + </dependency> + + <!-- execute as user sickbeard --> + <method_context> + <method_credential user='sickbeard' group='other' /> + </method_context> + + <exec_method + type='method' + name='start' + exec='/opt/sickbeard/SickBeard.py --daemon' + timeout_seconds='60'> + </exec_method> + + <exec_method + type='method' + name='stop' + exec=':kill' + timeout_seconds='2'> + </exec_method> + + <template> + <common_name> + <loctext xml:lang='C'>Sickbeard</loctext> + </common_name> + <documentation> + <doc_link name='sickbeard' + uri='http://www.sickbeard.com/' /> + </documentation> + </template> + + </service> + +</service_bundle> + diff --git a/lib/httplib2/__init__.py b/lib/httplib2/__init__.py index 39b78f4d1eeae608d99602ac2c33583990eefa21..01151f7fa257f7747fb2ecab6d27035533d68426 100644 --- a/lib/httplib2/__init__.py +++ b/lib/httplib2/__init__.py @@ -660,7 +660,7 @@ class FileCache(object): f = file(cacheFullPath, "rb") retval = f.read() f.close() - except IOError: + except IOError, e: pass return retval @@ -1048,7 +1048,7 @@ a string that contains the response entity body. feedparser.feed(info) info = feedparser.close() feedparser._parse = None - except IndexError: + except IndexError, ValueError: self.cache.delete(cachekey) cachekey = None cached_value = None @@ -1147,7 +1147,7 @@ a string that contains the response entity body. content = e.content response.status = 500 response.reason = str(e) - elif isinstance(e, socket.timeout): + elif isinstance(e, socket.timeout) or (isinstance(e, socket.error) and 'timed out' in str(e)): content = "Request Timeout" response = Response( { "content-type": "text/plain", diff --git a/lib/tvdb_api/tvdb_api.py b/lib/tvdb_api/tvdb_api.py index 00902c91dca3a130fab529c467c2856cd204595a..fa3ece36d08cd5cd03743369afd4f6a2af438632 100644 --- a/lib/tvdb_api/tvdb_api.py +++ b/lib/tvdb_api/tvdb_api.py @@ -29,6 +29,7 @@ import logging import datetime import time import traceback +import socket try: import xml.etree.cElementTree as ElementTree @@ -519,7 +520,7 @@ class Tvdb: try: log().debug("Retrieving URL %s" % url) header, resp = h.request(url, headers=h_header) - except (IOError, httplib2.HttpLib2Error), errormsg: + except (socket.error, IOError, httplib2.HttpLib2Error), errormsg: if not str(errormsg).startswith('HTTP Error'): lastTimeout = datetime.datetime.now() raise tvdb_error("Could not connect to server %s: %s" % (url, errormsg)) diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 930c36b0f6928a3e06df6ec7867638803b235f3d..dc2dcf4b7bcfd9c5aaa8b24275b195064f804cd2 100644 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -30,7 +30,7 @@ from threading import Lock # apparently py2exe won't build these unless they're imported somewhere from sickbeard import providers, metadata -from providers import ezrss, tvtorrents, nzbs_org, nzbmatrix, tvbinz, nzbsrus, newznab, womble, newzbin +from providers import ezrss, tvtorrents, nzbs_org, nzbmatrix, nzbsrus, newznab, womble, newzbin from sickbeard import searchCurrent, searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser from sickbeard import helpers, db, exceptions, show_queue, search_queue, scheduler @@ -56,6 +56,9 @@ MY_FULLNAME = None MY_NAME = None MY_ARGS = [] SYS_ENCODING = '' +DATA_DIR = '' +CREATEPID = False +PIDFILE = '' DAEMON = None @@ -95,6 +98,7 @@ WEB_IPV6 = None LAUNCH_BROWSER = None CACHE_DIR = None +ACTUAL_CACHE_DIR = None ROOT_DIRS = None USE_BANNER = None @@ -152,12 +156,6 @@ KEEP_PROCESSED_DIR = False MOVE_ASSOCIATED_FILES = False TV_DOWNLOAD_DIR = None -SHOW_TVBINZ = False -TVBINZ = False -TVBINZ_UID = None -TVBINZ_HASH = None -TVBINZ_AUTH = None - NZBS = False NZBS_UID = None NZBS_HASH = None @@ -180,7 +178,7 @@ SAB_USERNAME = None SAB_PASSWORD = None SAB_APIKEY = None SAB_CATEGORY = None -SAB_HOST = None +SAB_HOST = '' NZBGET_PASSWORD = None NZBGET_CATEGORY = None @@ -191,7 +189,7 @@ XBMC_NOTIFY_ONSNATCH = False XBMC_NOTIFY_ONDOWNLOAD = False XBMC_UPDATE_LIBRARY = False XBMC_UPDATE_FULL = False -XBMC_HOST = None +XBMC_HOST = '' XBMC_USERNAME = None XBMC_PASSWORD = None @@ -207,7 +205,7 @@ PLEX_PASSWORD = None USE_GROWL = False GROWL_NOTIFY_ONSNATCH = False GROWL_NOTIFY_ONDOWNLOAD = False -GROWL_HOST = None +GROWL_HOST = '' GROWL_PASSWORD = None USE_PROWL = False @@ -239,7 +237,6 @@ NMJ_HOST = None NMJ_DATABASE = None NMJ_MOUNT = None - COMING_EPS_LAYOUT = None COMING_EPS_DISPLAY_PAUSED = None COMING_EPS_SORT = None @@ -248,6 +245,8 @@ EXTRA_SCRIPTS = [] GIT_PATH = None +IGNORE_WORDS = "german,french,core2hd,dutch,swedish" + __INITIALIZED__ = False def CheckSection(sec): @@ -329,7 +328,7 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True): def get_backlog_cycle_time(): - cycletime = sickbeard.SEARCH_FREQUENCY*2+7 + cycletime = SEARCH_FREQUENCY*2+7 return max([cycletime, 720]) @@ -338,7 +337,7 @@ def initialize(consoleLogging=True): with INIT_LOCK: global LOG_DIR, WEB_PORT, WEB_LOG, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, \ - USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, TVBINZ, TVBINZ_UID, TVBINZ_HASH, DOWNLOAD_PROPERS, \ + USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, \ SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_HOST, \ NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_HOST, currentSearchScheduler, backlogSearchScheduler, \ USE_XBMC, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_UPDATE_FULL, \ @@ -353,8 +352,8 @@ def initialize(consoleLogging=True): USE_GROWL, GROWL_HOST, GROWL_PASSWORD, USE_PROWL, PROWL_NOTIFY_ONSNATCH, PROWL_NOTIFY_ONDOWNLOAD, PROWL_API, PROWL_PRIORITY, PROG_DIR, NZBMATRIX, NZBMATRIX_USERNAME, \ NZBMATRIX_APIKEY, versionCheckScheduler, VERSION_NOTIFY, PROCESS_AUTOMATICALLY, \ KEEP_PROCESSED_DIR, TV_DOWNLOAD_DIR, TVDB_BASE_URL, MIN_SEARCH_FREQUENCY, \ - TVBINZ_AUTH, showQueueScheduler, searchQueueScheduler, ROOT_DIRS, \ - NAMING_SHOW_NAME, NAMING_EP_TYPE, NAMING_MULTI_EP_TYPE, CACHE_DIR, TVDB_API_PARMS, \ + showQueueScheduler, searchQueueScheduler, ROOT_DIRS, \ + NAMING_SHOW_NAME, NAMING_EP_TYPE, NAMING_MULTI_EP_TYPE, CACHE_DIR, ACTUAL_CACHE_DIR, TVDB_API_PARMS, \ RENAME_EPISODES, properFinderScheduler, PROVIDER_ORDER, autoPostProcesserScheduler, \ NAMING_EP_NAME, NAMING_SEP_TYPE, NAMING_USE_PERIODS, WOMBLE, \ NZBSRUS, NZBSRUS_UID, NZBSRUS_HASH, NAMING_QUALITY, providerList, newznabProviderList, \ @@ -363,7 +362,7 @@ def initialize(consoleLogging=True): USE_LIBNOTIFY, LIBNOTIFY_NOTIFY_ONSNATCH, LIBNOTIFY_NOTIFY_ONDOWNLOAD, USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, \ USE_BANNER, USE_LISTVIEW, METADATA_XBMC, METADATA_MEDIABROWSER, METADATA_PS3, metadata_provider_dict, \ NEWZBIN, NEWZBIN_USERNAME, NEWZBIN_PASSWORD, GIT_PATH, MOVE_ASSOCIATED_FILES, \ - COMING_EPS_LAYOUT, COMING_EPS_SORT, COMING_EPS_DISPLAY_PAUSED, METADATA_WDTV + COMING_EPS_LAYOUT, COMING_EPS_SORT, COMING_EPS_DISPLAY_PAUSED, METADATA_WDTV, IGNORE_WORDS if __INITIALIZED__: return False @@ -373,7 +372,6 @@ def initialize(consoleLogging=True): CheckSection('General') CheckSection('Blackhole') CheckSection('Newzbin') - CheckSection('TVBinz') CheckSection('SABnzbd') CheckSection('NZBget') CheckSection('XBMC') @@ -403,10 +401,17 @@ def initialize(consoleLogging=True): WEB_PASSWORD = check_setting_str(CFG, 'General', 'web_password', '') LAUNCH_BROWSER = bool(check_setting_int(CFG, 'General', 'launch_browser', 1)) - CACHE_DIR = check_setting_str(CFG, 'General', 'cache_dir', 'cache') + ACTUAL_CACHE_DIR = check_setting_str(CFG, 'General', 'cache_dir', 'cache') # fix bad configs due to buggy code - if CACHE_DIR == 'None': - CACHE_DIR = 'cache' + if ACTUAL_CACHE_DIR == 'None': + ACTUAL_CACHE_DIR = 'cache' + + # unless they specify, put the cache dir inside the data dir + if not os.path.isabs(ACTUAL_CACHE_DIR): + CACHE_DIR = os.path.join(DATA_DIR, ACTUAL_CACHE_DIR) + else: + CACHE_DIR = ACTUAL_CACHE_DIR + if not helpers.makeDir(CACHE_DIR): logger.log(u"!!! Creating local cache dir failed, using system default", logger.ERROR) CACHE_DIR = None @@ -483,11 +488,6 @@ def initialize(consoleLogging=True): TVTORRENTS_DIGEST = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_digest', '') TVTORRENTS_HASH = check_setting_str(CFG, 'TVTORRENTS', 'tvtorrents_hash', '') - TVBINZ = bool(check_setting_int(CFG, 'TVBinz', 'tvbinz', 0)) - TVBINZ_UID = check_setting_str(CFG, 'TVBinz', 'tvbinz_uid', '') - TVBINZ_HASH = check_setting_str(CFG, 'TVBinz', 'tvbinz_hash', '') - TVBINZ_AUTH = check_setting_str(CFG, 'TVBinz', 'tvbinz_auth', '') - NZBS = bool(check_setting_int(CFG, 'NZBs', 'nzbs', 0)) NZBS_UID = check_setting_str(CFG, 'NZBs', 'nzbs_uid', '') NZBS_HASH = check_setting_str(CFG, 'NZBs', 'nzbs_hash', '') @@ -571,6 +571,8 @@ def initialize(consoleLogging=True): GIT_PATH = check_setting_str(CFG, 'General', 'git_path', '') + IGNORE_WORDS = check_setting_str(CFG, 'General', 'ignore_words', IGNORE_WORDS) + EXTRA_SCRIPTS = [x for x in check_setting_str(CFG, 'General', 'extra_scripts', '').split('|') if x] USE_BANNER = bool(check_setting_int(CFG, 'General', 'use_banner', 0)) @@ -637,7 +639,7 @@ def initialize(consoleLogging=True): providerList = providers.makeProviderList() - logger.initLogging(consoleLogging=consoleLogging) + logger.sb_log_instance.initLogging(consoleLogging=consoleLogging) # initialize the main SB database db.upgradeDatabase(db.DBConnection(), mainDB.InitialSchema) @@ -653,12 +655,6 @@ def initialize(consoleLogging=True): threadName="SEARCH", runImmediately=True) - backlogSearchScheduler = searchBacklog.BacklogSearchScheduler(searchBacklog.BacklogSearcher(), - cycleTime=datetime.timedelta(minutes=get_backlog_cycle_time()), - threadName="BACKLOG", - runImmediately=False) - backlogSearchScheduler.action.cycleTime = BACKLOG_SEARCH_FREQUENCY - # the interval for this is stored inside the ShowUpdater class showUpdaterInstance = showUpdater.ShowUpdater() showUpdateScheduler = scheduler.Scheduler(showUpdaterInstance, @@ -692,6 +688,12 @@ def initialize(consoleLogging=True): threadName="POSTPROCESSER", runImmediately=True) + backlogSearchScheduler = searchBacklog.BacklogSearchScheduler(searchBacklog.BacklogSearcher(), + cycleTime=datetime.timedelta(minutes=get_backlog_cycle_time()), + threadName="BACKLOG", + runImmediately=True) + backlogSearchScheduler.action.cycleTime = BACKLOG_SEARCH_FREQUENCY + showList = [] loadingShowList = {} @@ -839,27 +841,29 @@ def saveAndShutdown(restart=False): logger.log(u"Killing cherrypy") cherrypy.engine.exit() - if sickbeard.CREATEPID: - logger.log(u"Removing pidfile " + str(sickbeard.PIDFILE)) - os.remove(sickbeard.PIDFILE) + if CREATEPID: + logger.log(u"Removing pidfile " + str(PIDFILE)) + os.remove(PIDFILE) if restart: - install_type = sickbeard.versionCheckScheduler.action.install_type + install_type = versionCheckScheduler.action.install_type popen_list = [] if install_type in ('git', 'source'): - popen_list = [sys.executable, sickbeard.MY_FULLNAME] + popen_list = [sys.executable, MY_FULLNAME] elif install_type == 'win': if hasattr(sys, 'frozen'): # c:\dir\to\updater.exe 12345 c:\dir\to\sickbeard.exe - popen_list = [os.path.join(sickbeard.PROG_DIR, 'updater.exe'), str(sickbeard.PID), sys.executable] + popen_list = [os.path.join(PROG_DIR, 'updater.exe'), str(PID), sys.executable] else: logger.log(u"Unknown SB launch method, please file a bug report about this", logger.ERROR) - popen_list = [sys.executable, os.path.join(sickbeard.PROG_DIR, 'updater.py'), str(sickbeard.PID), sys.executable, sickbeard.MY_FULLNAME ] + popen_list = [sys.executable, os.path.join(PROG_DIR, 'updater.py'), str(PID), sys.executable, MY_FULLNAME ] if popen_list: - popen_list += sickbeard.MY_ARGS + popen_list += MY_ARGS + if '--nolaunch' not in popen_list: + popen_list += ['--nolaunch'] logger.log(u"Restarting Sick Beard with " + str(popen_list)) subprocess.Popen(popen_list, cwd=os.getcwd()) @@ -867,16 +871,17 @@ def saveAndShutdown(restart=False): def invoke_command(to_call, *args, **kwargs): + global invoked_command def delegate(): to_call(*args, **kwargs) - sickbeard.invoked_command = delegate - logger.log(u"Placed invoked command: "+repr(sickbeard.invoked_command)+" for "+repr(to_call)+" with "+repr(args)+" and "+repr(kwargs), logger.DEBUG) + invoked_command = delegate + logger.log(u"Placed invoked command: "+repr(invoked_command)+" for "+repr(to_call)+" with "+repr(args)+" and "+repr(kwargs), logger.DEBUG) def invoke_restart(soft=True): - invoke_command(sickbeard.restart, soft=soft) + invoke_command(restart, soft=soft) def invoke_shutdown(): - invoke_command(sickbeard.saveAndShutdown) + invoke_command(saveAndShutdown) def restart(soft=True): @@ -897,7 +902,7 @@ def restart(soft=True): def save_config(): new_config = ConfigObj() - new_config.filename = sickbeard.CONFIG_FILE + new_config.filename = CONFIG_FILE new_config['General'] = {} new_config['General']['log_dir'] = LOG_DIR @@ -937,7 +942,7 @@ def save_config(): new_config['General']['metadata_ps3'] = metadata_provider_dict['Sony PS3'].get_config() new_config['General']['metadata_wdtv'] = metadata_provider_dict['WDTV'].get_config() - new_config['General']['cache_dir'] = CACHE_DIR if CACHE_DIR else 'cache' + new_config['General']['cache_dir'] = ACTUAL_CACHE_DIR if ACTUAL_CACHE_DIR else 'cache' new_config['General']['root_dirs'] = ROOT_DIRS if ROOT_DIRS else '' new_config['General']['tv_download_dir'] = TV_DOWNLOAD_DIR new_config['General']['keep_processed_dir'] = int(KEEP_PROCESSED_DIR) @@ -947,6 +952,7 @@ def save_config(): new_config['General']['extra_scripts'] = '|'.join(EXTRA_SCRIPTS) new_config['General']['git_path'] = GIT_PATH + new_config['General']['ignore_words'] = IGNORE_WORDS new_config['Blackhole'] = {} new_config['Blackhole']['nzb_dir'] = NZB_DIR @@ -960,12 +966,6 @@ def save_config(): new_config['TVTORRENTS']['tvtorrents_digest'] = TVTORRENTS_DIGEST new_config['TVTORRENTS']['tvtorrents_hash'] = TVTORRENTS_HASH - new_config['TVBinz'] = {} - new_config['TVBinz']['tvbinz'] = int(TVBINZ) - new_config['TVBinz']['tvbinz_uid'] = TVBINZ_UID - new_config['TVBinz']['tvbinz_hash'] = TVBINZ_HASH - new_config['TVBinz']['tvbinz_auth'] = TVBINZ_AUTH - new_config['NZBs'] = {} new_config['NZBs']['nzbs'] = int(NZBS) new_config['NZBs']['nzbs_uid'] = NZBS_UID @@ -1089,7 +1089,7 @@ def getEpList(epIDs, showid=None): if epIDs == None or len(epIDs) == 0: return [] - query = "SELECT * FROM tv_episodes WHERE tvdbid in (%s)" % (",".join(["?" for x in epIDs]),) + query = "SELECT * FROM tv_episodes WHERE tvdbid in (%s)" % (",".join(['?']*len(epIDs)),) params = epIDs if showid != None: @@ -1102,7 +1102,7 @@ def getEpList(epIDs, showid=None): epList = [] for curEp in sqlResults: - curShowObj = helpers.findCertainShow(sickbeard.showList, int(curEp["showid"])) + curShowObj = helpers.findCertainShow(showList, int(curEp["showid"])) curEpObj = curShowObj.getEpisode(int(curEp["season"]), int(curEp["episode"])) epList.append(curEpObj) diff --git a/sickbeard/autoPostProcesser.py b/sickbeard/autoPostProcesser.py index 0bcee76de8c1d8bab7c24f7828b9dbb6d1254869..4bfc35575060916cebda6f5fb6af59141d075844 100644 --- a/sickbeard/autoPostProcesser.py +++ b/sickbeard/autoPostProcesser.py @@ -16,16 +16,13 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. +import os.path + import sickbeard -from sickbeard import helpers -from sickbeard import logger +from sickbeard import logger from sickbeard import encodingKludge as ek from sickbeard import processTV -from sickbeard import db - -import os.path - class PostProcesser(): @@ -41,9 +38,4 @@ class PostProcesser(): logger.log(u"Automatic post-processing attempted but dir "+sickbeard.TV_DOWNLOAD_DIR+" is relative (and probably not what you really want to process)", logger.ERROR) return - myDB = db.DBConnection() - sqlResults = myDB.select("SELECT * FROM tv_shows WHERE location = ? OR location LIKE ?", - [os.path.abspath(sickbeard.TV_DOWNLOAD_DIR), - ek.ek(os.path.join, os.path.abspath(sickbeard.TV_DOWNLOAD_DIR), '%')]) - processTV.processDir(sickbeard.TV_DOWNLOAD_DIR) diff --git a/sickbeard/browser.py b/sickbeard/browser.py index 69f89e4e13383ea9a27b3eb20b8095ce6955ab8b..c89728e137ae992b31a6f27b581f7d7e88056202 100644 --- a/sickbeard/browser.py +++ b/sickbeard/browser.py @@ -17,7 +17,6 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import os -import glob import string import cherrypy @@ -38,7 +37,7 @@ def getWinDrives(): assert os.name == 'nt' drives = [] - bitmask = windll.kernel32.GetLogicalDrives() + bitmask = windll.kernel32.GetLogicalDrives() #@UndefinedVariable for letter in string.uppercase: if bitmask & 1: drives.append(letter) diff --git a/sickbeard/classes.py b/sickbeard/classes.py index 3c2cf32acf699003d3896463e287c333727ba2ae..3a50950e6b3cc03d28ea0a076f8718ce7577e860 100644 --- a/sickbeard/classes.py +++ b/sickbeard/classes.py @@ -23,9 +23,7 @@ import sickbeard import urllib import datetime -from common import * - -from sickbeard import providers +from common import USER_AGENT class SickBeardURLopener(urllib.FancyURLopener): version = USER_AGENT diff --git a/sickbeard/common.py b/sickbeard/common.py index 9692dadd78cbaeae361505294a77352807d32660..f6d69abb733a1fb8e4674188f277dc46e0c2f08d 100644 --- a/sickbeard/common.py +++ b/sickbeard/common.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import sickbeard import os.path import operator, platform import re diff --git a/sickbeard/config.py b/sickbeard/config.py index f67a3fa15af1dd41a8342e446483ee5827ce8918..e7683b5861c7b10c2f6af8001b05d9f288a77add 100644 --- a/sickbeard/config.py +++ b/sickbeard/config.py @@ -46,7 +46,7 @@ def change_LOG_DIR(log_dir): if os.path.normpath(sickbeard.LOG_DIR) != os.path.normpath(log_dir): if helpers.makeDir(log_dir): sickbeard.LOG_DIR = os.path.normpath(log_dir) - logger.initLogging() + logger.sb_log_instance.initLogging() logger.log(u"Initialized new log file in " + log_dir) cherry_log = os.path.join(sickbeard.LOG_DIR, "cherrypy.log") @@ -132,5 +132,5 @@ def change_VERSION_NOTIFY(version_notify): sickbeard.NEWEST_VERSION_STRING = None; if oldSetting == False and version_notify == True: - sickbeard.versionCheckScheduler.action.run() + sickbeard.versionCheckScheduler.action.run() #@UndefinedVariable diff --git a/sickbeard/databases/mainDB.py b/sickbeard/databases/mainDB.py index 2acb6fe20b36a67115ed9627f56baf8430219166..e78e8748f9d89c2fb7f9d5ae2b3098553c9f9440 100644 --- a/sickbeard/databases/mainDB.py +++ b/sickbeard/databases/mainDB.py @@ -25,8 +25,7 @@ from sickbeard import logger from sickbeard.providers.generic import GenericProvider from sickbeard import encodingKludge as ek - - +from sickbeard.exceptions import ex class MainSanityCheck(db.DBSanityCheck): @@ -131,17 +130,17 @@ class NewQualitySettings (NumericProviders): def execute(self): numTries = 0 - while not ek.ek(os.path.isfile, ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard.db.v0')): - if not ek.ek(os.path.isfile, ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard.db')): + while not ek.ek(os.path.isfile, db.dbFilename(suffix='v0')): + if not ek.ek(os.path.isfile, db.dbFilename()): break try: logger.log(u"Attempting to back up your sickbeard.db file before migration...") - shutil.copy(ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard.db'), ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard.db.v0')) + shutil.copy(db.dbFilename(), db.dbFilename(suffix='v0')) logger.log(u"Done backup, proceeding with migration.") break except Exception, e: - logger.log(u"Error while trying to back up your sickbeard.db: "+str(e).decode('utf-8')) + logger.log(u"Error while trying to back up your sickbeard.db: "+ex(e)) numTries += 1 time.sleep(1) logger.log(u"Trying again.") @@ -212,7 +211,7 @@ class NewQualitySettings (NumericProviders): # if no updates were done then the backup is useless if didUpdate: - os.remove(ek.ek(os.path.join, sickbeard.PROG_DIR, 'sickbeard.db.v0')) + os.remove(db.dbFilename(suffix='v0')) ### Update show qualities @@ -398,4 +397,4 @@ class FixAirByDateSetting(SetNzbTorrentSettings): if cur_show["genre"] and "talk show" in cur_show["genre"].lower(): self.connection.action("UPDATE tv_shows SET air_by_date = ? WHERE tvdb_id = ?", [1, cur_show["tvdb_id"]]) - self.incDBVersion() \ No newline at end of file + self.incDBVersion() diff --git a/sickbeard/db.py b/sickbeard/db.py index 905617e2fc672df76489a803e584a65d6639cf57..5e63b28a2f980bcf45ef474cd85891d7580e7262 100644 --- a/sickbeard/db.py +++ b/sickbeard/db.py @@ -26,147 +26,160 @@ import threading import sickbeard +from sickbeard import encodingKludge as ek from sickbeard import logger +from sickbeard.exceptions import ex db_lock = threading.Lock() -class DBConnection: - def __init__(self, dbFileName="sickbeard.db"): - - self.dbFileName = dbFileName - - self.connection = sqlite3.connect(os.path.join(sickbeard.PROG_DIR, self.dbFileName), 20) - self.connection.row_factory = sqlite3.Row - - def action(self, query, args=None): - - with db_lock: - - if query == None: - return - - sqlResult = None - attempt = 0 - - while attempt < 5: - try: - if args == None: - logger.log(self.dbFileName+": "+query, logger.DEBUG) - sqlResult = self.connection.execute(query) - else: - logger.log(self.dbFileName+": "+query+" with args "+str(args), logger.DEBUG) - sqlResult = self.connection.execute(query, args) - self.connection.commit() - # get out of the connection attempt loop since we were successful - break - except sqlite3.OperationalError, e: - if "unable to open database file" in str(e) or "database is locked" in str(e): - logger.log(u"DB error: "+str(e).decode('utf-8'), logger.WARNING) - attempt += 1 - time.sleep(1) - else: - logger.log(u"DB error: "+str(e).decode('utf-8'), logger.ERROR) - raise - except sqlite3.DatabaseError, e: - logger.log(u"Fatal error executing query: " + str(e), logger.ERROR) - raise - - return sqlResult - - - def select(self, query, args=None): - - sqlResults = self.action(query, args).fetchall() +def dbFilename(filename="sickbeard.db", suffix=None): + """ + @param filename: The sqlite database filename to use. If not specified, + will be made to be sickbeard.db + @param suffix: The suffix to append to the filename. A '.' will be added + automatically, i.e. suffix='v0' will make dbfile.db.v0 + @return: the correct location of the database file. + """ + if suffix: + filename = "%s.%s" % (filename, suffix) + return ek.ek(os.path.join, sickbeard.DATA_DIR, filename) - if sqlResults == None: - return [] - - return sqlResults - - def upsert(self, tableName, valueDict, keyDict): - - changesBefore = self.connection.total_changes - - genParams = lambda myDict : [x + " = ?" for x in myDict.keys()] - - query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) - - self.action(query, valueDict.values() + keyDict.values()) - - if self.connection.total_changes == changesBefore: - query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \ - " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" - self.action(query, valueDict.values() + keyDict.values()) - - def tableInfo(self, tableName): - # FIXME ? binding is not supported here, but I cannot find a way to escape a string manually - cursor = self.connection.execute("PRAGMA table_info(%s)" % tableName) - columns = {} - for column in cursor: - columns[column['name']] = { 'type': column['type'] } - return columns +class DBConnection: + def __init__(self, filename="sickbeard.db", suffix=None): + + self.filename = filename + self.connection = sqlite3.connect(dbFilename(filename), 20) + self.connection.row_factory = sqlite3.Row + + def action(self, query, args=None): + + with db_lock: + + if query == None: + return + + sqlResult = None + attempt = 0 + + while attempt < 5: + try: + if args == None: + logger.log(self.filename+": "+query, logger.DEBUG) + sqlResult = self.connection.execute(query) + else: + logger.log(self.filename+": "+query+" with args "+str(args), logger.DEBUG) + sqlResult = self.connection.execute(query, args) + self.connection.commit() + # get out of the connection attempt loop since we were successful + break + except sqlite3.OperationalError, e: + if "unable to open database file" in e.message or "database is locked" in e.message: + logger.log(u"DB error: "+ex(e), logger.WARNING) + attempt += 1 + time.sleep(1) + else: + logger.log(u"DB error: "+ex(e), logger.ERROR) + raise + except sqlite3.DatabaseError, e: + logger.log(u"Fatal error executing query: " + ex(e), logger.ERROR) + raise + + return sqlResult + + + def select(self, query, args=None): + + sqlResults = self.action(query, args).fetchall() + + if sqlResults == None: + return [] + + return sqlResults + + def upsert(self, tableName, valueDict, keyDict): + + changesBefore = self.connection.total_changes + + genParams = lambda myDict : [x + " = ?" for x in myDict.keys()] + + query = "UPDATE "+tableName+" SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) + + self.action(query, valueDict.values() + keyDict.values()) + + if self.connection.total_changes == changesBefore: + query = "INSERT INTO "+tableName+" (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + \ + " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" + self.action(query, valueDict.values() + keyDict.values()) + + def tableInfo(self, tableName): + # FIXME ? binding is not supported here, but I cannot find a way to escape a string manually + cursor = self.connection.execute("PRAGMA table_info(%s)" % tableName) + columns = {} + for column in cursor: + columns[column['name']] = { 'type': column['type'] } + return columns def sanityCheckDatabase(connection, sanity_check): - sanity_check(connection).check() + sanity_check(connection).check() class DBSanityCheck(object): - def __init__(self, connection): - self.connection = connection + def __init__(self, connection): + self.connection = connection - def check(self): - pass + def check(self): + pass # =============== # = Upgrade API = # =============== def upgradeDatabase(connection, schema): - logger.log(u"Checking database structure...", logger.MESSAGE) - _processUpgrade(connection, schema) + logger.log(u"Checking database structure...", logger.MESSAGE) + _processUpgrade(connection, schema) def prettyName(str): - return ' '.join([x.group() for x in re.finditer("([A-Z])([a-z0-9]+)", str)]) + return ' '.join([x.group() for x in re.finditer("([A-Z])([a-z0-9]+)", str)]) def _processUpgrade(connection, upgradeClass): - instance = upgradeClass(connection) - logger.log(u"Checking " + prettyName(upgradeClass.__name__) + " database upgrade", logger.DEBUG) - if not instance.test(): - logger.log(u"Database upgrade required: " + prettyName(upgradeClass.__name__), logger.MESSAGE) - try: - instance.execute() - except sqlite3.DatabaseError, e: - print "Error in " + str(upgradeClass.__name__) + ": " + str(e) - raise - logger.log(upgradeClass.__name__ + " upgrade completed", logger.DEBUG) - else: - logger.log(upgradeClass.__name__ + " upgrade not required", logger.DEBUG) - - for upgradeSubClass in upgradeClass.__subclasses__(): - _processUpgrade(connection, upgradeSubClass) + instance = upgradeClass(connection) + logger.log(u"Checking " + prettyName(upgradeClass.__name__) + " database upgrade", logger.DEBUG) + if not instance.test(): + logger.log(u"Database upgrade required: " + prettyName(upgradeClass.__name__), logger.MESSAGE) + try: + instance.execute() + except sqlite3.DatabaseError, e: + print "Error in " + str(upgradeClass.__name__) + ": " + ex(e) + raise + logger.log(upgradeClass.__name__ + " upgrade completed", logger.DEBUG) + else: + logger.log(upgradeClass.__name__ + " upgrade not required", logger.DEBUG) + + for upgradeSubClass in upgradeClass.__subclasses__(): + _processUpgrade(connection, upgradeSubClass) # Base migration class. All future DB changes should be subclassed from this class class SchemaUpgrade (object): - def __init__(self, connection): - self.connection = connection - - def hasTable(self, tableName): - return len(self.connection.action("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, )).fetchall()) > 0 - - def hasColumn(self, tableName, column): - return column in self.connection.tableInfo(tableName) - - def addColumn(self, table, column, type="NUMERIC", default=0): - self.connection.action("ALTER TABLE %s ADD %s %s" % (table, column, type)) - self.connection.action("UPDATE %s SET %s = ?" % (table, column), (default,)) - - def checkDBVersion(self): - result = self.connection.select("SELECT db_version FROM db_version") - if result: - return int(result[0]["db_version"]) - else: - return 0 - - def incDBVersion(self): - curVersion = self.checkDBVersion() - self.connection.action("UPDATE db_version SET db_version = ?", [curVersion+1]) - return curVersion+1 + def __init__(self, connection): + self.connection = connection + + def hasTable(self, tableName): + return len(self.connection.action("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, )).fetchall()) > 0 + + def hasColumn(self, tableName, column): + return column in self.connection.tableInfo(tableName) + + def addColumn(self, table, column, type="NUMERIC", default=0): + self.connection.action("ALTER TABLE %s ADD %s %s" % (table, column, type)) + self.connection.action("UPDATE %s SET %s = ?" % (table, column), (default,)) + + def checkDBVersion(self): + result = self.connection.select("SELECT db_version FROM db_version") + if result: + return int(result[0]["db_version"]) + else: + return 0 + + def incDBVersion(self): + curVersion = self.checkDBVersion() + self.connection.action("UPDATE db_version SET db_version = ?", [curVersion+1]) + return curVersion+1 diff --git a/sickbeard/encodingKludge.py b/sickbeard/encodingKludge.py index 6b505808c3d12f2b6d891f44e684c40fd71503b5..c2ea784257688c3a9dc5519247defccd70c77d2a 100644 --- a/sickbeard/encodingKludge.py +++ b/sickbeard/encodingKludge.py @@ -17,8 +17,6 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import os -import os.path -import locale from sickbeard import logger import sickbeard diff --git a/sickbeard/exceptions.py b/sickbeard/exceptions.py index a72d13140509ccd240750888f613e6739fbb869c..52e328c40088d32b07cc9ce0ebeb520af187f1c2 100644 --- a/sickbeard/exceptions.py +++ b/sickbeard/exceptions.py @@ -16,7 +16,28 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - +from sickbeard.encodingKludge import fixStupidEncodings + +def ex(e): + """ + Returns a unicode string from the exception text if it exists. + """ + + # sanity check + if not e.args or not e.args[0]: + return "" + + e_message = fixStupidEncodings(e.args[0]) + + # if fixStupidEncodings doesn't fix it then maybe it's not a string, in which case we'll try printing it anyway + if not e_message: + try: + e_message = str(e.args[0]) + except: + e_message = "" + + return e_message + class SickBeardException(Exception): "Generic SickBeard Exception - should never be thrown, only subclassed" diff --git a/sickbeard/generic_queue.py b/sickbeard/generic_queue.py index dc543bfcaf5e7b0dd1b48c89b91d9bb5559f0999..eb1d3801c1ec519ddececa1804ab9ee0e50ecd97 100644 --- a/sickbeard/generic_queue.py +++ b/sickbeard/generic_queue.py @@ -16,10 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import operator +import datetime import threading -import sickbeard from sickbeard import logger class QueuePriorities: @@ -51,7 +50,10 @@ class GenericQueue(object): self.min_priority = 0 def add_item(self, item): + item.added = datetime.datetime.now() self.queue.append(item) + + return item def run(self): @@ -67,7 +69,21 @@ class GenericQueue(object): if len(self.queue) > 0: # sort by priority - self.queue.sort(key=operator.attrgetter('priority'), reverse=True) + def sorter(x,y): + """ + Sorts by priority descending then time ascending + """ + if x.priority == y.priority: + if y.added == x.added: + return 0 + elif y.added < x.added: + return 1 + elif y.added > x.added: + return -1 + else: + return y.priority-x.priority + + self.queue.sort(cmp=sorter) queueItem = self.queue[0] @@ -96,6 +112,8 @@ class QueueItem: self.thread_name = None self.action_id = action_id + + self.added = None def get_thread_name(self): if self.thread_name: diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 47cbc0ab5ad59f68646636bb61722168a9e7850d..959f0815181afb0d42fa505bb7e804a4cb1c014e 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -18,20 +18,21 @@ import StringIO, zlib, gzip -import os.path, os +import os import stat import urllib, urllib2 -import re +import re, socket import shutil import sickbeard -from sickbeard.exceptions import * +from sickbeard.exceptions import MultipleShowObjectsException from sickbeard import logger, classes -from sickbeard.common import * +from sickbeard.common import USER_AGENT, mediaExtensions, XML_NSMAP from sickbeard import db from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from lib.tvdb_api import tvdb_api, tvdb_exceptions @@ -124,17 +125,21 @@ def getURL (url, headers=[]): encoding = usock.info().get("Content-Encoding") - if encoding in ('gzip', 'x-gzip', 'deflate'): - content = usock.read() - if encoding == 'deflate': - data = StringIO.StringIO(zlib.decompress(content)) - else: - data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) - result = data.read() + try: + if encoding in ('gzip', 'x-gzip', 'deflate'): + content = usock.read() + if encoding == 'deflate': + data = StringIO.StringIO(zlib.decompress(content)) + else: + data = gzip.GzipFile('', 'rb', 9, StringIO.StringIO(content)) + result = data.read() - else: - result = usock.read() - usock.close() + else: + result = usock.read() + usock.close() + except socket.timeout: + logger.log(u"Timed out while loading URL "+url, logger.WARNING) + return None return result @@ -407,7 +412,7 @@ def rename_file(old_path, new_name): try: ek.ek(os.rename, old_path, new_path) except (OSError, IOError), e: - logger.log(u"Failed renaming " + old_path + " to " + new_path + ": " + str(e), logger.ERROR) + logger.log(u"Failed renaming " + old_path + " to " + new_path + ": " + ex(e), logger.ERROR) return False return new_path @@ -417,6 +422,11 @@ def chmodAsParent(childPath): return parentPath = ek.ek(os.path.dirname, childPath) + + if not parentPath: + logger.log(u"No parent path provided in "+childPath+", unable to get permissions from it", logger.DEBUG) + return + parentMode = stat.S_IMODE(os.stat(parentPath)[stat.ST_MODE]) if ek.ek(os.path.isfile, childPath): @@ -453,7 +463,7 @@ def fixSetGroupID(childPath): return try: - ek.ek(os.chown, childPath, -1, parentGID) + ek.ek(os.chown, childPath, -1, parentGID) #@UndefinedVariable - only available on UNIX logger.log(u"Respecting the set-group-ID bit on the parent directory for %s" % (childPath), logger.DEBUG) except OSError: logger.log(u"Failed to respect the set-group-ID bit on the parent directory for %s (setting group ID %i)" % (childPath, parentGID), logger.ERROR) diff --git a/sickbeard/history.py b/sickbeard/history.py index a2eb4ce97e9025421b4d5983d7dfe992cfd9e0f0..87cd9d427c6e394b6e86627406800263bb381d9d 100644 --- a/sickbeard/history.py +++ b/sickbeard/history.py @@ -17,12 +17,9 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import db -import sqlite3 import datetime -from sickbeard import logger -from sickbeard.common import * -from sickbeard import providers +from sickbeard.common import SNATCHED, Quality dateFormat = "%Y%m%d%H%M%S" diff --git a/sickbeard/image_cache.py b/sickbeard/image_cache.py index 4f9640235751b67b951edcc853dcd916361ba837..0a485a5a21a15261edaa21bdaf18da45a6521476 100644 --- a/sickbeard/image_cache.py +++ b/sickbeard/image_cache.py @@ -17,8 +17,6 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import os.path -import sys -import traceback import sickbeard @@ -214,7 +212,7 @@ class ImageCache: logger.log(u"Found an image in the show dir that doesn't exist in the cache, caching it: "+cur_file_name+", type "+str(cur_file_type), logger.DEBUG) self._cache_image_from_file(cur_file_name, cur_file_type, show_obj.tvdbid) need_images[cur_file_type] = False - except exceptions.ShowDirNotFoundException, e: + except exceptions.ShowDirNotFoundException: logger.log(u"Unable to search for images in show dir because it doesn't exist", logger.WARNING) # download from TVDB for missing ones diff --git a/sickbeard/logger.py b/sickbeard/logger.py index 8e1ca30a19cf2c378d5250574f3c42caf0ad1217..28bdc5e8985c54987efc4c3bf8634a8b402220d5 100644 --- a/sickbeard/logger.py +++ b/sickbeard/logger.py @@ -18,18 +18,22 @@ from __future__ import with_statement -import os.path +import os import threading import logging -import logging.handlers - -from exceptions import * import sickbeard from sickbeard import classes + +# number of log files to keep +NUM_LOGS = 3 + +# log size in bytes +LOG_SIZE = 10000000 # 10 megs + ERROR = logging.ERROR WARNING = logging.WARNING MESSAGE = logging.INFO @@ -40,63 +44,136 @@ reverseNames = {u'ERROR': ERROR, u'INFO': MESSAGE, u'DEBUG': DEBUG} -logFile = '' - -log_lock = threading.Lock() - -def initLogging(consoleLogging=True): - global logFile - - logFile = os.path.join(sickbeard.LOG_DIR, 'sickbeard.log') - - fileHandler = logging.handlers.RotatingFileHandler( - logFile, - maxBytes=25000000, - backupCount=5) - - fileHandler.setLevel(logging.DEBUG) - fileHandler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%b-%d %H:%M:%S')) - - logging.getLogger('sickbeard').addHandler(fileHandler) - - # define a Handler which writes INFO messages or higher to the sys.stderr - if consoleLogging: - console = logging.StreamHandler() +class SBRotatingLogHandler(object): - console.setLevel(logging.INFO) - - # set a format which is simpler for console use - console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S')) - - # add the handler to the root logger - logging.getLogger('sickbeard').addHandler(console) - - logging.getLogger('sickbeard').setLevel(logging.DEBUG) + def __init__(self, log_file, num_files, num_bytes): + self.num_files = num_files + self.num_bytes = num_bytes + + self.log_file = log_file + self.cur_handler = None -def log(toLog, logLevel=MESSAGE): + self.writes_since_check = 0 - with log_lock: + self.log_lock = threading.Lock() - meThread = threading.currentThread().getName() - message = meThread + u" :: " + toLog + def initLogging(self, consoleLogging=True): + + self.log_file = os.path.join(sickbeard.LOG_DIR, self.log_file) + + self.cur_handler = self._config_handler() + + logging.getLogger('sickbeard').addHandler(self.cur_handler) + + # define a Handler which writes INFO messages or higher to the sys.stderr + if consoleLogging: + console = logging.StreamHandler() + + console.setLevel(logging.INFO) - outLine = message.encode('utf-8') + # set a format which is simpler for console use + console.setFormatter(logging.Formatter('%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S')) - sbLogger = logging.getLogger('sickbeard') + # add the handler to the root logger + logging.getLogger('sickbeard').addHandler(console) + + logging.getLogger('sickbeard').setLevel(logging.DEBUG) - try: - if logLevel == DEBUG: - sbLogger.debug(outLine) - elif logLevel == MESSAGE: - sbLogger.info(outLine) - elif logLevel == WARNING: - sbLogger.warning(outLine) - elif logLevel == ERROR: - sbLogger.error(outLine) + def _config_handler(self): + """ + Configure a file handler to log at file_name and return it. + """ + + file_handler = logging.FileHandler(self.log_file) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%b-%d %H:%M:%S')) + return file_handler + + def _log_file_name(self, i): + """ + Returns a numbered log file name depending on i. If i==0 it just uses logName, if not it appends + it to the extension (blah.log.3 for i == 3) + + i: Log number to ues + """ + return self.log_file + ('.' + str(i) if i else '') + + def _num_logs(self): + """ + Scans the log folder and figures out how many log files there are already on disk + + Returns: The number of the last used file (eg. mylog.log.3 would return 3). If there are no logs it returns -1 + """ + cur_log = 0 + while os.path.isfile(self._log_file_name(cur_log)): + cur_log += 1 + return cur_log - 1 + + def _rotate_logs(self): + + sb_logger = logging.getLogger('sickbeard') + + # delete the old handler + if self.cur_handler: + self.cur_handler.flush() + self.cur_handler.close() + sb_logger.removeHandler(self.cur_handler) + + # rename or delete all the old log files + for i in range(self._num_logs(), -1, -1): + cur_file_name = self._log_file_name(i) + try: + if i >= NUM_LOGS: + os.remove(cur_file_name) + else: + os.rename(cur_file_name, self._log_file_name(i+1)) + except WindowsError: + pass + + # the new log handler will always be on the un-numbered .log file + new_file_handler = self._config_handler() - # add errors to the UI logger - classes.ErrorViewer.add(classes.UIError(message)) + self.cur_handler = new_file_handler + + sb_logger.addHandler(new_file_handler) + + def log(self, toLog, logLevel=MESSAGE): + + with self.log_lock: + + # check the size and see if we need to rotate + if self.writes_since_check >= 10: + if os.path.isfile(self.log_file) and os.path.getsize(self.log_file) >= LOG_SIZE: + self._rotate_logs() + self.writes_since_check = 0 else: - sbLogger.log(logLevel, outLine) - except ValueError, e: - pass \ No newline at end of file + self.writes_since_check += 1 + + meThread = threading.currentThread().getName() + message = meThread + u" :: " + toLog + + out_line = message.encode('utf-8') + + sb_logger = logging.getLogger('sickbeard') + + try: + if logLevel == DEBUG: + sb_logger.debug(out_line) + elif logLevel == MESSAGE: + sb_logger.info(out_line) + elif logLevel == WARNING: + sb_logger.warning(out_line) + elif logLevel == ERROR: + sb_logger.error(out_line) + + # add errors to the UI logger + classes.ErrorViewer.add(classes.UIError(message)) + else: + sb_logger.log(logLevel, out_line) + except ValueError: + pass + +sb_log_instance = SBRotatingLogHandler('sickbeard.log', NUM_LOGS, LOG_SIZE) + +def log(toLog, logLevel=MESSAGE): + sb_log_instance.log(toLog, logLevel) \ No newline at end of file diff --git a/sickbeard/metadata/generic.py b/sickbeard/metadata/generic.py index c31150a343d300f6227cc611ecabeec89fb222bb..b303aca8de098ef3ba71618d5a7d7c5bbb806fb0 100644 --- a/sickbeard/metadata/generic.py +++ b/sickbeard/metadata/generic.py @@ -24,17 +24,15 @@ import re import sickbeard -from sickbeard.common import * -from sickbeard import logger, exceptions, helpers -from sickbeard import encodingKludge as ek +from sickbeard import exceptions, helpers from sickbeard.metadata import helpers as metadata_helpers +from sickbeard import logger +from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from lib.tvdb_api import tvdb_api, tvdb_exceptions -from sickbeard import logger -from sickbeard import encodingKludge as ek - class GenericMetadata(): """ Base class for all metadata providers. Default behavior is meant to mostly @@ -244,9 +242,9 @@ class GenericMetadata(): t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) tvdb_show_obj = t[ep_obj.show.tvdbid] except tvdb_exceptions.tvdb_shownotfound, e: - raise exceptions.ShowNotFoundException(str(e)) + raise exceptions.ShowNotFoundException(e.message) except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+ex(e), logger.ERROR) return None # try all included episodes in case some have thumbs and others don't @@ -301,7 +299,7 @@ class GenericMetadata(): nfo_file.close() helpers.chmodAsParent(nfo_file_path) except IOError, e: - logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) return False return True @@ -345,7 +343,7 @@ class GenericMetadata(): nfo_file.close() helpers.chmodAsParent(nfo_file_path) except IOError, e: - logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to write file to "+nfo_file_path+" - are you sure the folder is writable? "+ex(e), logger.ERROR) return False return True @@ -453,7 +451,7 @@ class GenericMetadata(): continue # Just grab whatever's there for now - art_id, season_url = cur_season_art.popitem() + art_id, season_url = cur_season_art.popitem() #@UnusedVariable season_thumb_file_path = self.get_season_thumb_path(show_obj, cur_season) @@ -502,7 +500,7 @@ class GenericMetadata(): outFile.close() helpers.chmodAsParent(image_path) except IOError, e: - logger.log(u"Unable to write image to "+image_path+" - are you sure the show folder is writable? "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to write image to "+image_path+" - are you sure the show folder is writable? "+ex(e), logger.ERROR) return False return True @@ -531,7 +529,7 @@ class GenericMetadata(): t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) tvdb_show_obj = t[show_obj.tvdbid] except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"Unable to look up show on TVDB, not downloading images: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) return None if image_type not in ('fanart', 'poster', 'banner'): @@ -568,7 +566,7 @@ class GenericMetadata(): t = tvdb_api.Tvdb(banners=True, **ltvdb_api_parms) tvdb_show_obj = t[show_obj.tvdbid] except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"Unable to look up show on TVDB, not downloading images: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to look up show on TVDB, not downloading images: "+ex(e), logger.ERROR) return result # How many seasons? @@ -635,7 +633,7 @@ class GenericMetadata(): return empty_return except (exceptions.NoNFOException, SyntaxError, ValueError), e: - logger.log(u"There was an error parsing your existing metadata file: " + str(e), logger.WARNING) + logger.log(u"There was an error parsing your existing metadata file: " + ex(e), logger.WARNING) return empty_return return (tvdb_id, name) diff --git a/sickbeard/metadata/helpers.py b/sickbeard/metadata/helpers.py index f773016aab46f2f92e17c21813dff88d1114e568..5acd822de7c4f4be7e78a0ff4d92c7b04f954a76 100644 --- a/sickbeard/metadata/helpers.py +++ b/sickbeard/metadata/helpers.py @@ -17,14 +17,13 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import os -import os.path import urllib2 import sickbeard -from sickbeard.common import * from sickbeard import logger, exceptions, helpers from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from lib.tvdb_api import tvdb_api, tvdb_exceptions @@ -32,8 +31,6 @@ import xml.etree.cElementTree as etree def getTVDBIDFromNFO(dir): - show_lang = None - if not ek.ek(os.path.isdir, dir): logger.log(u"Show dir doesn't exist, can't load NFO") raise exceptions.NoNFOException("The show dir doesn't exist, no NFO could be loaded") @@ -52,7 +49,6 @@ def getTVDBIDFromNFO(dir): + str(showXML.findtext('tvdbid')) + " " \ + str(showXML.findtext('id'))) - name = showXML.findtext('title') if showXML.findtext('tvdbid') != None: tvdb_id = int(showXML.findtext('tvdbid')) elif showXML.findtext('id'): @@ -69,14 +65,14 @@ def getTVDBIDFromNFO(dir): raise exceptions.NoNFOException("Unable to look up the show on TVDB, not using the NFO") except (exceptions.NoNFOException, SyntaxError, ValueError), e: - logger.log(u"There was an error parsing your existing tvshow.nfo file: " + str(e), logger.ERROR) + logger.log(u"There was an error parsing your existing tvshow.nfo file: " + ex(e), logger.ERROR) logger.log(u"Attempting to rename it to tvshow.nfo.old", logger.DEBUG) try: xmlFileObj.close() ek.ek(os.rename, xmlFile, xmlFile + ".old") except Exception, e: - logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + str(e), logger.ERROR) + logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + ex(e), logger.ERROR) raise exceptions.NoNFOException("Invalid info in tvshow.nfo") return tvdb_id @@ -101,7 +97,7 @@ def getShowImage(url, imgNum=None): logger.log(u"There was an error trying to retrieve the image, aborting", logger.ERROR) return None except urllib2.HTTPError, e: - logger.log(u"Unable to access image at "+tempURL+", assuming it doesn't exist: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to access image at "+tempURL+", assuming it doesn't exist: "+ex(e), logger.ERROR) return None return image_data diff --git a/sickbeard/metadata/mediabrowser.py b/sickbeard/metadata/mediabrowser.py index e5f2629ca64c30339a99ba13207c041b6767ef9b..cf9536ee5d64dd6b79a7fecb0f04475b19e6fc9d 100644 --- a/sickbeard/metadata/mediabrowser.py +++ b/sickbeard/metadata/mediabrowser.py @@ -16,16 +16,19 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import datetime, re +import datetime +import os +import re import sickbeard import generic -from sickbeard.common import * +from sickbeard.common import XML_NSMAP from sickbeard import logger, exceptions, helpers from sickbeard import encodingKludge as ek from lib.tvdb_api import tvdb_api, tvdb_exceptions +from sickbeard.exceptions import ex import xml.etree.cElementTree as etree @@ -264,8 +267,6 @@ class MediaBrowserMetadata(generic.GenericMetadata): eps_to_write = [ep_obj] + ep_obj.relatedEps - shouldSave = False - tvdb_lang = ep_obj.show.lang try: @@ -279,9 +280,9 @@ class MediaBrowserMetadata(generic.GenericMetadata): t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) myShow = t[ep_obj.show.tvdbid] except tvdb_exceptions.tvdb_shownotfound, e: - raise exceptions.ShowNotFoundException(str(e)) + raise exceptions.ShowNotFoundException(e.message) except tvdb_exceptions.tvdb_error, e: - logger.log("Unable to connect to TVDB while creating meta files - skipping - "+str(e), logger.ERROR) + logger.log("Unable to connect to TVDB while creating meta files - skipping - "+ex(e), logger.ERROR) return False rootNode = etree.Element("Item") diff --git a/sickbeard/metadata/ps3.py b/sickbeard/metadata/ps3.py index 6fa2d34cb3353f6cd7579ac53412b6c93f25af60..39c1f3d0d553f516a9be897af7c11c800781c672 100644 --- a/sickbeard/metadata/ps3.py +++ b/sickbeard/metadata/ps3.py @@ -16,16 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import datetime - -import sickbeard +import os import generic -from sickbeard.common import * -from sickbeard import logger, exceptions, helpers -from lib.tvdb_api import tvdb_api, tvdb_exceptions - from sickbeard import encodingKludge as ek class PS3Metadata(generic.GenericMetadata): diff --git a/sickbeard/metadata/wdtv.py b/sickbeard/metadata/wdtv.py index b06a39801574968700bceecc0002c51af9f0e7a7..94c5dced06350fecfb1a73c255fec3f08510fdf7 100644 --- a/sickbeard/metadata/wdtv.py +++ b/sickbeard/metadata/wdtv.py @@ -16,15 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import datetime - -import sickbeard +import os +import re import generic -from sickbeard.common import * -from sickbeard import logger, exceptions, helpers -from lib.tvdb_api import tvdb_api, tvdb_exceptions +from sickbeard import logger, helpers from sickbeard import encodingKludge as ek diff --git a/sickbeard/metadata/xbmc.py b/sickbeard/metadata/xbmc.py index 8dd648fcd05906472d81184e8728e01fb17cd15d..f1d760a35f2b620b5179319884e023aa06c72be4 100644 --- a/sickbeard/metadata/xbmc.py +++ b/sickbeard/metadata/xbmc.py @@ -22,9 +22,10 @@ import sickbeard import generic -from sickbeard.common import * +from sickbeard.common import XML_NSMAP from sickbeard import logger, exceptions, helpers -from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex + from lib.tvdb_api import tvdb_api, tvdb_exceptions import xml.etree.cElementTree as etree @@ -188,9 +189,9 @@ class XBMCMetadata(generic.GenericMetadata): t = tvdb_api.Tvdb(actors=True, **ltvdb_api_parms) myShow = t[ep_obj.show.tvdbid] except tvdb_exceptions.tvdb_shownotfound, e: - raise exceptions.ShowNotFoundException(str(e)) + raise exceptions.ShowNotFoundException(e.message) except tvdb_exceptions.tvdb_error, e: - logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to connect to TVDB while creating meta files - skipping - "+ex(e), logger.ERROR) return if len(eps_to_write) > 1: diff --git a/sickbeard/name_cache.py b/sickbeard/name_cache.py index a0629f4ce226eeaa28ad992a94b790aa0cc913b0..d29e1bee19db66ce4f40ab0c798016ff402927bc 100644 --- a/sickbeard/name_cache.py +++ b/sickbeard/name_cache.py @@ -19,8 +19,6 @@ from sickbeard import db from sickbeard.helpers import sanitizeSceneName -from sickbeard import logger - def addNameToCache(name, tvdb_id): """ Adds the show & tvdb id to the scene_names table in cache.db. diff --git a/sickbeard/name_parser/parser.py b/sickbeard/name_parser/parser.py index dd8c31f5f79d73477702fb221adf7e5e4c8d025e..ed844bcdea1b501b37a8d586d4cfc5d11997ff3a 100644 --- a/sickbeard/name_parser/parser.py +++ b/sickbeard/name_parser/parser.py @@ -22,6 +22,8 @@ import re import regexes +import sickbeard + from sickbeard import logger class NameParser(object): @@ -111,7 +113,7 @@ class NameParser(object): try: result.air_date = datetime.date(year, month, day) except ValueError, e: - raise InvalidNameException(str(e)) + raise InvalidNameException(e.message) if 'extra_info' in named_groups: tmp_extra_info = match.group('extra_info') @@ -228,7 +230,7 @@ class NameParser(object): # if there's no useful info in it then raise an exception if final_result.season_number == None and not final_result.episode_numbers and final_result.air_date == None and not final_result.series_name: - raise InvalidNameException("Unable to parse "+name) + raise InvalidNameException("Unable to parse "+name.encode(sickbeard.SYS_ENCODING)) # return it return final_result diff --git a/sickbeard/notifiers/growl.py b/sickbeard/notifiers/growl.py index 68f94edde83d7df98b2442eb235fe6a9d16c3823..d4bcc1a143e3e828bdaa7aab038b93543f76daad 100644 --- a/sickbeard/notifiers/growl.py +++ b/sickbeard/notifiers/growl.py @@ -17,17 +17,18 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import socket -import sys import sickbeard from sickbeard import logger, common +from sickbeard.exceptions import ex from lib.growl import gntp class GrowlNotifier: def test_notify(self, host, password): + self._sendRegistration(host, password, 'Test') return self._sendGrowl("Test Growl", "Testing Growl settings from Sick Beard", "Test", host, password, force=True) def notify_snatch(self, ep_name): @@ -39,17 +40,7 @@ class GrowlNotifier: self._sendGrowl(common.notifyStrings[common.NOTIFY_DOWNLOAD], ep_name) def _send_growl(self, options,message=None): - - #Send Registration - register = gntp.GNTPRegister() - register.add_header('Application-Name',options['app']) - register.add_notification(options['name'],True) - - if options['password']: - register.set_password(options['password']) - - self._send(options['host'],options['port'],register.encode(),options['debug']) - + #Send Notification notice = gntp.GNTPNotice() @@ -67,7 +58,7 @@ class GrowlNotifier: if options['priority']: notice.add_header('Notification-Priority',options['priority']) if options['icon']: - notice.add_header('Notification-Icon',options['icon']) + notice.add_header('Notification-Icon', 'https://github.com/midgetspy/Sick-Beard/raw/master/data/images/sickbeard_touch_icon.png') if message: notice.add_header('Notification-Text',message) @@ -79,7 +70,6 @@ class GrowlNotifier: def _send(self, host,port,data,debug=False): if debug: print '<Sending>\n',data,'\n</Sending>' - response = '' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(data) @@ -91,7 +81,6 @@ class GrowlNotifier: return response def _sendGrowl(self, title="Sick Beard Notification", message=None, name=None, host=None, password=None, force=False): - if not sickbeard.USE_GROWL and not force: return False @@ -126,7 +115,7 @@ class GrowlNotifier: else: opts['password'] = password - opts['icon'] = False + opts['icon'] = True for pc in growlHosts: @@ -136,7 +125,53 @@ class GrowlNotifier: try: return self._send_growl(opts, message) except socket.error, e: - logger.log(u"Unable to send growl to "+opts['host']+":"+str(opts['port'])+": "+str(e).decode('utf-8')) + logger.log(u"Unable to send growl to "+opts['host']+":"+str(opts['port'])+": "+ex(e)) return False + def _sendRegistration(self, host=None, password=None, name='Sick Beard Notification'): + opts = {} + + if host == None: + hostParts = sickbeard.GROWL_HOST.split(':') + else: + hostParts = host.split(':') + + if len(hostParts) != 2 or hostParts[1] == '': + port = 23053 + else: + port = int(hostParts[1]) + + opts['host'] = hostParts[0] + opts['port'] = port + + + if password == None: + opts['password'] = sickbeard.GROWL_PASSWORD + else: + opts['password'] = password + + + opts['app'] = 'SickBeard' + opts['debug'] = False + + #Send Registration + register = gntp.GNTPRegister() + register.add_header('Application-Name', opts['app']) + register.add_header('Application-Icon', 'https://github.com/midgetspy/Sick-Beard/raw/master/data/images/sickbeard_touch_icon.png') + + register.add_notification('Test', True) + register.add_notification(common.notifyStrings[common.NOTIFY_SNATCH], True) + register.add_notification(common.notifyStrings[common.NOTIFY_DOWNLOAD], True) + + if opts['password']: + register.set_password(opts['password']) + + try: + return self._send(opts['host'],opts['port'],register.encode(),opts['debug']) + except socket.error, e: + logger.log(u"Unable to send growl to "+opts['host']+":"+str(opts['port'])+": "+str(e).decode('utf-8')) + return False + + + notifier = GrowlNotifier \ No newline at end of file diff --git a/sickbeard/notifiers/libnotify.py b/sickbeard/notifiers/libnotify.py index 13cda96abfb0cbd8dce730cac574045a7118a378..a95f7d9deef758e00f58849cbd695039359e4e85 100644 --- a/sickbeard/notifiers/libnotify.py +++ b/sickbeard/notifiers/libnotify.py @@ -28,7 +28,7 @@ def diagnose(): user-readable message indicating possible issues. ''' try: - import pynotify + import pynotify #@UnusedImport except ImportError: return (u"<p>Error: pynotify isn't installed. On Ubuntu/Debian, install the " u"<a href=\"apt:python-notify\">python-notify</a> package.") @@ -58,6 +58,7 @@ def diagnose(): class LibnotifyNotifier: def __init__(self): self.pynotify = None + self.gobject = None def init_pynotify(self): if self.pynotify is not None: @@ -67,10 +68,16 @@ class LibnotifyNotifier: except ImportError: logger.log(u"Unable to import pynotify. libnotify notifications won't work.") return False + try: + import gobject + except ImportError: + logger.log(u"Unable to import gobject. We can't catch a GError in display.") + return False if not pynotify.init('Sick Beard'): logger.log(u"Initialization of pynotify failed. libnotify notifications won't work.") return False self.pynotify = pynotify + self.gobject = gobject return True def notify_snatch(self, ep_name): @@ -99,7 +106,9 @@ class LibnotifyNotifier: # will be printed but the call to show() will still return True. # pynotify doesn't seem too keen on error handling. n = self.pynotify.Notification(title, message, icon_uri) - return n.show() - + try: + return n.show() + except self.gobject.GError: + return False notifier = LibnotifyNotifier diff --git a/sickbeard/notifiers/notifo.py b/sickbeard/notifiers/notifo.py index 24b904b639355906eae921f9e3f69f7fc181adb1..f525c4f777a27f0985176a7dcdda9727fbaf2c7a 100644 --- a/sickbeard/notifiers/notifo.py +++ b/sickbeard/notifiers/notifo.py @@ -1,4 +1,5 @@ # Author: Nic Wolfe <nic@wolfeden.ca> +# Revised by: Shawn Conroyd - 4/12/2011 # URL: http://code.google.com/p/sickbeard/ # # This file is part of Sick Beard. @@ -17,32 +18,34 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import urllib + import sickbeard -from sickbeard import logger, common +from sickbeard import logger try: - import lib.simplejson as json + import lib.simplejson as json #@UnusedImport except: - import json - + import json #@Reimport API_URL = "https://%(username)s:%(secret)s@api.notifo.com/v1/send_notification" class NotifoNotifier: - def test_notify(self, username, apisecret): - return self._sendNotifo("This is a test notification from Sick Beard", username, apisecret) + def test_notify(self, username, apisecret, title="Test:"): + return self._sendNotifo("This is a test notification from SickBeard", title, username, apisecret) - def _sendNotifo(self, msg, username, apisecret): + def _sendNotifo(self, msg, title, username, apisecret, label="SickBeard"): msg = msg.strip() apiurl = API_URL % {"username": username, "secret": apisecret} data = urllib.urlencode({ - "msg": msg, + "title": title, + "label": label, + "msg": msg }) try: - data = urllib.urlopen(apiurl, data) + data = urllib.urlopen(apiurl, data) result = json.load(data) except IOError: return False @@ -55,15 +58,15 @@ class NotifoNotifier: return True - def notify_snatch(self, ep_name): + def notify_snatch(self, ep_name, title="Snatched:"): if sickbeard.NOTIFO_NOTIFY_ONSNATCH: - self._notifyNotifo(common.notifyStrings[common.NOTIFY_SNATCH]+': '+ep_name) + self._notifyNotifo(title, ep_name) - def notify_download(self, ep_name): + def notify_download(self, ep_name, title="Completed:"): if sickbeard.NOTIFO_NOTIFY_ONDOWNLOAD: - self._notifyNotifo(common.notifyStrings[common.NOTIFY_DOWNLOAD]+': '+ep_name) + self._notifyNotifo(title, ep_name) - def _notifyNotifo(self, message=None, username=None, apisecret=None, force=False): + def _notifyNotifo(self, title, message=None, username=None, apisecret=None, force=False): if not sickbeard.USE_NOTIFO and not force: logger.log("Notification for Notifo not enabled, skipping this notification", logger.DEBUG) return False @@ -75,7 +78,7 @@ class NotifoNotifier: logger.log(u"Sending notification for " + message, logger.DEBUG) - self._sendNotifo(message, username, apisecret) + self._sendNotifo(message, title, username, apisecret) return True -notifier = NotifoNotifier +notifier = NotifoNotifier \ No newline at end of file diff --git a/sickbeard/notifiers/plex.py b/sickbeard/notifiers/plex.py index 1401d45460a464f01a851c2b69d2a57000b552ad..2ca071e6546ea989fa24fb0bcf71e67f82bac739 100644 --- a/sickbeard/notifiers/plex.py +++ b/sickbeard/notifiers/plex.py @@ -19,9 +19,11 @@ import urllib import sickbeard -from xml.dom import minidom from sickbeard import logger, common from sickbeard.notifiers.xbmc import XBMCNotifier +from sickbeard.exceptions import ex + +from xml.dom import minidom class PLEXNotifier(XBMCNotifier): @@ -67,7 +69,12 @@ class PLEXNotifier(XBMCNotifier): logger.log(u"Plex Media Server updating " + sickbeard.PLEX_SERVER_HOST, logger.DEBUG) url = "http://%s/library/sections" % sickbeard.PLEX_SERVER_HOST - xml_sections = minidom.parse(urllib.urlopen(url)) + try: + xml_sections = minidom.parse(urllib.urlopen(url)) + except IOError, e: + logger.log(u"Error while trying to contact your plex server: "+ex(e), logger.ERROR) + return False + sections = xml_sections.getElementsByTagName('Directory') for s in sections: @@ -75,9 +82,9 @@ class PLEXNotifier(XBMCNotifier): url = "http://%s/library/sections/%s/refresh" % (sickbeard.PLEX_SERVER_HOST, s.getAttribute('key')) try: - x = urllib.urlopen(url) + urllib.urlopen(url) except Exception, e: - logger.log(u"Error updating library section: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error updating library section: "+ex(e), logger.ERROR) return False return True diff --git a/sickbeard/notifiers/prowl.py b/sickbeard/notifiers/prowl.py index 6491db817872cf8e9dd30791f9c446a33027ead4..01e5447de4d87f74703653679582e02c916046e8 100644 --- a/sickbeard/notifiers/prowl.py +++ b/sickbeard/notifiers/prowl.py @@ -16,12 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import socket -import sys from httplib import HTTPSConnection from urllib import urlencode - import sickbeard from sickbeard import logger, common diff --git a/sickbeard/notifiers/tweet.py b/sickbeard/notifiers/tweet.py index 35a29027858bf4cd9cd9f0f7bf35fb4008f4f6ae..c56288038b60d84f66a2f6bae040534662567c0e 100644 --- a/sickbeard/notifiers/tweet.py +++ b/sickbeard/notifiers/tweet.py @@ -16,18 +16,16 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import socket -import os -import sys import sickbeard from sickbeard import logger, common +from sickbeard.exceptions import ex # parse_qsl moved to urlparse module in v2.6 try: - from urlparse import parse_qsl + from urlparse import parse_qsl #@UnusedImport except: - from cgi import parse_qsl + from cgi import parse_qsl #@Reimport import lib.oauth2 as oauth import lib.pythontwitter as twitter @@ -55,7 +53,7 @@ class TwitterNotifier: def _get_authorization(self): - signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) oauth_client = oauth.Client(oauth_consumer) @@ -85,7 +83,7 @@ class TwitterNotifier: logger.log('Generating and signing request for an access token using key '+key) - signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() + signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() #@UnusedVariable oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) logger.log('oauth_consumer: '+str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) @@ -120,9 +118,9 @@ class TwitterNotifier: api = twitter.Api(username, password, access_token_key, access_token_secret) try: - status = api.PostUpdate(message) + api.PostUpdate(message) except Exception, e: - logger.log(u"Error Sending Tweet: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error Sending Tweet: "+ex(e), logger.ERROR) return False return True diff --git a/sickbeard/notifiers/xbmc.py b/sickbeard/notifiers/xbmc.py index 8144b0fa4ccae32de6f67bfdd1e7dca6155808c0..f967e4b88c6a9d0301a1c6c49afbb3b768286883 100644 --- a/sickbeard/notifiers/xbmc.py +++ b/sickbeard/notifiers/xbmc.py @@ -19,16 +19,14 @@ import urllib, urllib2 import socket -import sys import base64 import time, struct -#import config - import sickbeard from sickbeard import logger from sickbeard import common +from sickbeard.exceptions import ex try: import xml.etree.cElementTree as etree @@ -107,8 +105,7 @@ class XBMCNotifier: response = handle.read() logger.log(u"response: " + response, logger.DEBUG) except IOError, e: - # print "Warning: Couldn't contact XBMC HTTP server at " + host + ": " + str(e) - logger.log(u"Warning: Couldn't contact XBMC HTTP server at " + host + ": " + str(e)) + logger.log(u"Warning: Couldn't contact XBMC HTTP server at " + host + ": " + ex(e)) response = '' return response @@ -182,7 +179,7 @@ class XBMCNotifier: try: et = etree.fromstring(encSqlXML) except SyntaxError, e: - logger.log("Unable to parse XML returned from XBMC: "+str(e), logger.ERROR) + logger.log("Unable to parse XML returned from XBMC: "+ex(e), logger.ERROR) return False paths = et.findall('.//field') @@ -235,7 +232,7 @@ def wakeOnLan(ethernet_address): # Test Connection function def isHostUp(host,port): - (family, socktype, proto, garbage, address) = socket.getaddrinfo(host, port)[0] + (family, socktype, proto, garbage, address) = socket.getaddrinfo(host, port)[0] #@UnusedVariable s = socket.socket(family, socktype, proto) try: diff --git a/sickbeard/nzbSplitter.py b/sickbeard/nzbSplitter.py index 9de912fc049d304a238474e1972bb3d8e7cfeeae..4664e047370eba479ebe493f45b53c02b56052ec 100644 --- a/sickbeard/nzbSplitter.py +++ b/sickbeard/nzbSplitter.py @@ -25,7 +25,7 @@ import re from name_parser.parser import NameParser, InvalidNameException from sickbeard import logger, classes, helpers -from sickbeard.common import * +from sickbeard.common import Quality def getSeasonNZBs(name, urlData, season): @@ -43,7 +43,7 @@ def getSeasonNZBs(name, urlData, season): sceneNameMatch = re.search(regex, filename, re.I) if sceneNameMatch: - showName, qualitySection, groupName = sceneNameMatch.groups() + showName, qualitySection, groupName = sceneNameMatch.groups() #@UnusedVariable else: logger.log(u"Unable to parse "+name+" into a scene name. If it's a valid one log a bug.", logger.ERROR) return ({},'') @@ -102,7 +102,7 @@ def splitResult(result): try: urlData = helpers.getURL(result.url) - except urllib2.URLError, e: + except urllib2.URLError: logger.log(u"Unable to load url "+result.url+", can't download season NZB", logger.ERROR) return False diff --git a/sickbeard/nzbget.py b/sickbeard/nzbget.py index 3d73d1b4cda2e97beba68010b8b81b991c20785b..950b4938c477fcb481824a09090bd88817bf807d 100644 --- a/sickbeard/nzbget.py +++ b/sickbeard/nzbget.py @@ -28,8 +28,7 @@ import xmlrpclib from sickbeard.providers.generic import GenericProvider -from sickbeard.common import * -from sickbeard import logger, classes +from sickbeard import logger def sendNZB(nzb): diff --git a/sickbeard/postProcessor.py b/sickbeard/postProcessor.py index 3dd8f229e6b443d8d7ff3a54102c5b7b9cd6e303..b0526b9c282a9ab8aca752ff5b9300f48a25ea5e 100755 --- a/sickbeard/postProcessor.py +++ b/sickbeard/postProcessor.py @@ -20,7 +20,6 @@ from __future__ import with_statement import glob import os -import os.path import re import shlex import subprocess @@ -39,6 +38,7 @@ from sickbeard import show_name_helpers from sickbeard import scene_exceptions from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from sickbeard.name_parser.parser import NameParser, InvalidNameException @@ -113,7 +113,7 @@ class PostProcessor(object): base_name = file_path.rpartition('.')[0]+'.' # don't confuse glob with chars we didn't mean to use - base_name = re.sub(r'[\[\]\*\?]', r'\\\g<0>', base_name) + base_name = re.sub(r'[\[\]\*\?]', r'[\g<0>]', base_name) for associated_file_path in ek.ek(glob.glob, base_name+'*'): # only list it if the only non-shared part is the extension @@ -169,18 +169,19 @@ class PostProcessor(object): cur_file_name = ek.ek(os.path.basename, cur_file_path) + # get the extension + cur_extension = cur_file_path.rpartition('.')[-1] + + # replace .nfo with .nfo-orig to avoid conflicts + if cur_extension == 'nfo': + cur_extension = 'nfo-orig' + # If new base name then convert name if new_base_name: - # get the extension - cur_extension = cur_file_path.rpartition('.')[-1] - - # replace .nfo with .nfo-orig to avoid conflicts - if cur_extension == 'nfo': - cur_extension = 'nfo-orig' - new_file_name = new_base_name +'.' + cur_extension + # if we're not renaming we still want to change extensions sometimes else: - new_file_name = cur_file_name + new_file_name = helpers.replaceExtension(cur_file_name, cur_extension) new_file_path = ek.ek(os.path.join, new_path, new_file_name) @@ -201,7 +202,7 @@ class PostProcessor(object): helpers.moveFile(cur_file_path, new_file_path) helpers.chmodAsParent(new_file_path) except (IOError, OSError), e: - self._log("Unable to move file "+cur_file_path+" to "+new_file_path+": "+str(e).decode('utf-8'), logger.ERROR) + self._log("Unable to move file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) raise e self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_move) @@ -221,7 +222,7 @@ class PostProcessor(object): helpers.copyFile(cur_file_path, new_file_path) helpers.chmodAsParent(new_file_path) except (IOError, OSError), e: - logger.log("Unable to copy file "+cur_file_path+" to "+new_file_path+": "+str(e).decode('utf-8'), logger.ERROR) + logger.log("Unable to copy file "+cur_file_path+" to "+new_file_path+": "+ex(e), logger.ERROR) raise e self._combined_file_operation(file_path, new_path, new_base_name, associated_files, action=_int_copy) @@ -252,7 +253,10 @@ class PostProcessor(object): if ep_obj.show.air_by_date: season_folder = str(ep_obj.airdate.year) else: - season_folder = sickbeard.SEASON_FOLDERS_FORMAT % (ep_obj.season) + try: + season_folder = sickbeard.SEASON_FOLDERS_FORMAT % (ep_obj.season) + except TypeError: + logger.log(u"Error: Your season folder format is incorrect, try setting it back to the default") dest_folder = ek.ek(os.path.join, ep_obj.show.location, season_folder) @@ -363,7 +367,7 @@ class PostProcessor(object): self._log(u"Looking up name "+cur_name+u" on TVDB", logger.DEBUG) showObj = t[cur_name] - except (tvdb_exceptions.tvdb_exception), e: + except (tvdb_exceptions.tvdb_exception): # if none found, search on all languages try: # There's gotta be a better way of doing this but we don't wanna @@ -375,11 +379,11 @@ class PostProcessor(object): self._log(u"Looking up name "+cur_name+u" in all languages on TVDB", logger.DEBUG) showObj = t[cur_name] - except (tvdb_exceptions.tvdb_exception, IOError), e: + except (tvdb_exceptions.tvdb_exception, IOError): pass continue - except (IOError), e: + except (IOError): continue self._log(u"Lookup successful, using tvdb id "+str(showObj["id"]), logger.DEBUG) @@ -423,10 +427,11 @@ class PostProcessor(object): try: (cur_tvdb_id, cur_season, cur_episodes) = cur_attempt() except InvalidNameException, e: - logger.log(u"Unable to parse, skipping: "+str(e), logger.DEBUG) + logger.log(u"Unable to parse, skipping: "+ex(e), logger.DEBUG) continue - if cur_tvdb_id: + # if we already did a successful history lookup then keep that tvdb_id value + if cur_tvdb_id and not (self.in_history and tvdb_id): tvdb_id = cur_tvdb_id if cur_season != None: season = cur_season @@ -434,7 +439,7 @@ class PostProcessor(object): episodes = cur_episodes # for air-by-date shows we need to look up the season/episode from tvdb - if season == -1 and tvdb_id: + if season == -1 and tvdb_id and episodes: self._log(u"Looks like this is an air-by-date show, attempting to convert the date to season/episode", logger.DEBUG) # try to get language set for this show @@ -505,7 +510,7 @@ class PostProcessor(object): try: curEp = show_obj.getEpisode(season, episode) except exceptions.EpisodeNotFoundException, e: - self._log(u"Unable to create episode: "+str(e).decode('utf-8'), logger.DEBUG) + self._log(u"Unable to create episode: "+ex(e), logger.DEBUG) raise exceptions.PostProcessingFailed() if root_ep == None: @@ -519,10 +524,10 @@ class PostProcessor(object): def _get_quality(self, ep_obj): ep_quality = common.Quality.UNKNOWN - oldStatus = None + # make sure the quality is set right before we continue if ep_obj.status in common.Quality.SNATCHED + common.Quality.SNATCHED_PROPER: - oldStatus, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) + oldStatus, ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable if ep_quality != common.Quality.UNKNOWN: self._log(u"The old status had a quality in it, using that: "+common.Quality.qualityStrings[ep_quality], logger.DEBUG) return ep_quality @@ -557,10 +562,10 @@ class PostProcessor(object): self._log(u"Absolute path to script: "+ek.ek(os.path.abspath, script_cmd[0]), logger.DEBUG) try: p = subprocess.Popen(script_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=sickbeard.PROG_DIR) - out, err = p.communicate() + out, err = p.communicate() #@UnusedVariable self._log(u"Script result: "+str(out), logger.DEBUG) except OSError, e: - self._log(u"Unable to run extra_script: "+str(e).decode('utf-8')) + self._log(u"Unable to run extra_script: "+ex(e)) def _is_priority(self, ep_obj, new_ep_quality): @@ -575,7 +580,7 @@ class PostProcessor(object): return True # if the user downloaded it manually and it appears to be a PROPER/REPACK then it's priority - old_ep_status, old_ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) + old_ep_status, old_ep_quality = common.Quality.splitCompositeStatus(ep_obj.status) #@UnusedVariable if self.is_proper and new_ep_quality >= old_ep_quality: self._log(u"This was manually downloaded but it appears to be a proper so I'm marking it as priority", logger.DEBUG) return True diff --git a/sickbeard/processTV.py b/sickbeard/processTV.py index 4a654461451955ebc3b1e809631ee1e02f982e04..95425f2c6a6d2607974fdfcf7f7380ac8d01e29c 100644 --- a/sickbeard/processTV.py +++ b/sickbeard/processTV.py @@ -18,16 +18,17 @@ from __future__ import with_statement -import os, os.path +import os import shutil +import sickbeard from sickbeard import postProcessor from sickbeard import db, helpers, exceptions from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from sickbeard import logger -from sickbeard.common import * def logHelper (logMessage, logLevel=logger.MESSAGE): logger.log(logMessage, logLevel) @@ -94,8 +95,10 @@ def processDir (dirName, nzbName=None, recurse=False): try: processor = postProcessor.PostProcessor(cur_video_file_path, nzbName) process_result = processor.process() - except exceptions.PostProcessingFailed: + process_fail_message = "" + except exceptions.PostProcessingFailed, e: process_result = False + process_fail_message = ex(e) returnStr += processor.log @@ -111,11 +114,11 @@ def processDir (dirName, nzbName=None, recurse=False): try: shutil.rmtree(dirName) except (OSError, IOError), e: - returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + str(e).decode('utf-8'), logger.ERROR) + returnStr += logHelper(u"Warning: unable to remove the folder " + dirName + ": " + ex(e), logger.WARNING) returnStr += logHelper(u"Processing succeeded for "+cur_video_file_path) else: - returnStr += logHelper(u"Processing failed for "+cur_video_file_path) + returnStr += logHelper(u"Processing failed for "+cur_video_file_path+": "+process_fail_message, logger.WARNING) return returnStr diff --git a/sickbeard/properFinder.py b/sickbeard/properFinder.py index e21c70354c0289419ce7f3c8605debe923065a18..3cbba8e7282819123c238d0e86cad55868dafa87 100644 --- a/sickbeard/properFinder.py +++ b/sickbeard/properFinder.py @@ -22,12 +22,12 @@ import operator import sickbeard from sickbeard import db -from sickbeard import classes, common, helpers, logger, show_name_helpers +from sickbeard import helpers, logger, show_name_helpers from sickbeard import providers from sickbeard import search from sickbeard import history -from sickbeard.common import * +from sickbeard.common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, Quality from lib.tvdb_api import tvdb_api, tvdb_exceptions @@ -105,7 +105,7 @@ class ProperFinder(): # populate our Proper instance if parse_result.air_by_date: - curProper.season == -1 + curProper.season = -1 curProper.episode = parse_result.air_date else: curProper.season = parse_result.season_number if parse_result.season_number != None else 1 @@ -162,9 +162,9 @@ class ProperFinder(): try: t = tvdb_api.Tvdb(**ltvdb_api_parms) epObj = t[curProper.tvdbid].airedOn(curProper.episode)[0] - season = int(epObj["seasonnumber"]) - episodes = [int(epObj["episodenumber"])] - except tvdb_exceptions.tvdb_episodenotfound, e: + curProper.season = int(epObj["seasonnumber"]) + curProper.episodes = [int(epObj["episodenumber"])] + except tvdb_exceptions.tvdb_episodenotfound: logger.log(u"Unable to find episode with date "+str(curProper.episode)+" for show "+parse_result.series_name+", skipping", logger.WARNING) continue @@ -232,6 +232,8 @@ class ProperFinder(): # snatch it downloadResult = search.snatchEpisode(result, SNATCHED_PROPER) + + return downloadResult def _genericName(self, name): return name.replace(".", " ").replace("-"," ").replace("_"," ").lower() diff --git a/sickbeard/providers/__init__.py b/sickbeard/providers/__init__.py index f642a1200abc97bb8b383f4c21f09b5b25abd6c6..30ea49029d6d904aac1f27d5e650e5a397de12f5 100644 --- a/sickbeard/providers/__init__.py +++ b/sickbeard/providers/__init__.py @@ -20,7 +20,6 @@ __all__ = ['ezrss', 'tvtorrents', 'nzbmatrix', 'nzbs_org', - 'tvbinz', 'nzbsrus', 'womble', 'newzbin', @@ -71,6 +70,11 @@ def getNewznabProviderList(data): providerDict[curDefault.name].default = True providerDict[curDefault.name].name = curDefault.name providerDict[curDefault.name].url = curDefault.url + + # a 0 in the key spot indicates that no key is needed, so set this on the object + if curDefault.key == '0': + curDefault.key = '' + curDefault.needs_auth = False return filter(lambda x: x, providerList) @@ -91,7 +95,7 @@ def makeNewznabProvider(configString): return newProvider def getDefaultNewznabProviders(): - return 'NZB.su|https://nzb.su/||0' + return 'Sick Beard Index|http://momo.sickbeard.com/|0|0' def getProviderModule(name): diff --git a/sickbeard/providers/ezrss.py b/sickbeard/providers/ezrss.py index f0d2ba9580ea7b28aca5915d360d09db93ec957d..9a645181357c4ada2ed16d66b3f4a97d272a53f4 100644 --- a/sickbeard/providers/ezrss.py +++ b/sickbeard/providers/ezrss.py @@ -24,10 +24,11 @@ import xml.etree.cElementTree as etree import sickbeard import generic -from sickbeard.common import * +from sickbeard.common import Quality from sickbeard import logger from sickbeard import tvcache from sickbeard.helpers import sanitizeSceneName +from sickbeard.exceptions import ex class EZRSSProvider(generic.TorrentProvider): @@ -117,7 +118,7 @@ class EZRSSProvider(generic.TorrentProvider): responseSoup = etree.ElementTree(etree.XML(data)) items = responseSoup.getiterator('item') except Exception, e: - logger.log(u"Error trying to load EZRSS RSS feed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to load EZRSS RSS feed: "+ex(e), logger.ERROR) logger.log(u"RSS data: "+data, logger.DEBUG) return [] diff --git a/sickbeard/providers/generic.py b/sickbeard/providers/generic.py index ced160fb7f8667d48213691209b2fd6fc3f0734a..ba28542d8d7c7671d32cdc2245d44b8793de989c 100644 --- a/sickbeard/providers/generic.py +++ b/sickbeard/providers/generic.py @@ -18,21 +18,20 @@ -import urllib2 -import os.path -import sys import datetime -import time - -import xml.etree.cElementTree as etree +import os +import sys +import re +import urllib2 import sickbeard -from sickbeard import helpers, classes, exceptions, logger, db +from sickbeard import helpers, classes, logger, db -from sickbeard.common import * +from sickbeard.common import Quality, MULTI_EP_RESULT, SEASON_RESULT from sickbeard import tvcache from sickbeard import encodingKludge as ek +from sickbeard.exceptions import ex from lib.hachoir_parser import createParser @@ -112,7 +111,7 @@ class GenericProvider: try: result = helpers.getURL(url, headers) except (urllib2.HTTPError, IOError), e: - logger.log(u"Error loading "+self.name+" URL: " + str(sys.exc_info()) + " - " + str(e), logger.ERROR) + logger.log(u"Error loading "+self.name+" URL: " + str(sys.exc_info()) + " - " + ex(e), logger.ERROR) return None return result @@ -150,7 +149,7 @@ class GenericProvider: fileOut.close() helpers.chmodAsParent(fileName) except IOError, e: - logger.log("Unable to save the file: "+str(e).decode('utf-8'), logger.ERROR) + logger.log("Unable to save the file: "+ex(e), logger.ERROR) return False # as long as it's a valid download then consider it a successful snatch @@ -164,9 +163,15 @@ class GenericProvider: # primitive verification of torrents, just make sure we didn't get a text file or something if self.providerType == GenericProvider.TORRENT: parser = createParser(file_name) - if not parser or parser._getMimeType() != 'application/x-bittorrent': - logger.log(u"Result is not a valid torrent file", logger.WARNING) - return False + if parser: + mime_type = parser._getMimeType() + try: + parser.stream._input.close() + except: + pass + if mime_type != 'application/x-bittorrent': + logger.log(u"Result is not a valid torrent file", logger.WARNING) + return False return True diff --git a/sickbeard/providers/newzbin.py b/sickbeard/providers/newzbin.py index e3d72ef18ea405aa94349a8c04108fa79bb4110b..f8284a139239e196a389d7333e37f8a261b3d677 100644 --- a/sickbeard/providers/newzbin.py +++ b/sickbeard/providers/newzbin.py @@ -16,20 +16,23 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. +import os import re +import sys import time import urllib -import sys import xml.etree.cElementTree as etree +from datetime import datetime, timedelta import sickbeard import generic import sickbeard.encodingKludge as ek -from sickbeard import classes, logger, helpers, exceptions, db, show_name_helpers +from sickbeard import classes, logger, helpers, exceptions, show_name_helpers from sickbeard import tvcache -from sickbeard.common import * +from sickbeard.common import Quality +from sickbeard.exceptions import ex class NewzbinDownloader(urllib.FancyURLopener): @@ -73,6 +76,8 @@ class NewzbinProvider(generic.NZBProvider): self.NEWZBIN_NS = 'http://www.newzbin.com/DTD/2007/feeds/report/' + self.NEWZBIN_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S %Z' + def _report(self, name): return '{'+self.NEWZBIN_NS+'}'+name @@ -217,7 +222,7 @@ class NewzbinProvider(generic.NZBProvider): logger.log("Done waiting for Newzbin API throttle limit, starting downloads again") self.downloadResult(nzb) except (urllib.ContentTooShortError, IOError), e: - logger.log("Error downloading NZB: " + str(sys.exc_info()) + " - " + str(e), logger.ERROR) + logger.log("Error downloading NZB: " + str(sys.exc_info()) + " - " + ex(e), logger.ERROR) return False return True @@ -228,7 +233,7 @@ class NewzbinProvider(generic.NZBProvider): try: f = myOpener.openit(url) except (urllib.ContentTooShortError, IOError), e: - logger.log("Error loading search results: " + str(sys.exc_info()) + " - " + str(e), logger.ERROR) + logger.log("Error loading search results: " + str(sys.exc_info()) + " - " + ex(e), logger.ERROR) return None data = f.read() @@ -273,13 +278,22 @@ class NewzbinProvider(generic.NZBProvider): responseSoup = etree.ElementTree(etree.XML(data)) items = responseSoup.getiterator('item') except Exception, e: - logger.log("Error trying to load Newzbin RSS feed: "+str(e), logger.ERROR) + logger.log("Error trying to load Newzbin RSS feed: "+ex(e), logger.ERROR) return [] for cur_item in items: title = cur_item.findtext('title') if title == 'Feed Error': raise exceptions.AuthException("The feed wouldn't load, probably because of invalid auth info") + if sickbeard.USENET_RETENTION is not None: + try: + post_date = datetime.strptime(cur_item.findtext('{http://www.newzbin.com/DTD/2007/feeds/report/}postdate'), self.NEWZBIN_DATE_FORMAT) + retention_date = datetime.now() - timedelta(days=sickbeard.USENET_RETENTION) + if post_date < retention_date: + continue + except Exception, e: + logger.log("Error parsing date from Newzbin RSS feed: " + str(e), logger.ERROR) + continue item_list.append(cur_item) diff --git a/sickbeard/providers/newznab.py b/sickbeard/providers/newznab.py index a1827531d73c9f074a4ba5fc6fd25b0edbd5ff08..6950b05ffe5abf9edbe875ebe6d4ba9724ed7120 100644 --- a/sickbeard/providers/newznab.py +++ b/sickbeard/providers/newznab.py @@ -21,6 +21,7 @@ import urllib import datetime import re +import os import xml.etree.cElementTree as etree @@ -29,11 +30,12 @@ import generic from sickbeard import classes from sickbeard.helpers import sanitizeSceneName +from sickbeard import encodingKludge as ek from sickbeard import exceptions -from sickbeard.common import * from sickbeard import logger from sickbeard import tvcache +from sickbeard.exceptions import ex class NewznabProvider(generic.NZBProvider): @@ -45,6 +47,9 @@ class NewznabProvider(generic.NZBProvider): self.url = url self.key = key + + # if a provider doesn't need an api key then this can be false + self.needs_auth = True self.enabled = True self.supportsBacklog = True @@ -55,6 +60,8 @@ class NewznabProvider(generic.NZBProvider): return self.name + '|' + self.url + '|' + self.key + '|' + str(int(self.enabled)) def imageName(self): + if ek.ek(os.path.isfile, ek.ek(os.path.join, sickbeard.PROG_DIR, 'data', 'images', 'providers', self.getID()+'.gif')): + return self.getID()+'.gif' return 'newznab.gif' def isEnabled(self): @@ -112,7 +119,6 @@ class NewznabProvider(generic.NZBProvider): return [params] - def _doGeneralSearch(self, search_string): return self._doSearch({'q': search_string}) @@ -147,7 +153,7 @@ class NewznabProvider(generic.NZBProvider): responseSoup = etree.ElementTree(etree.XML(data)) items = responseSoup.getiterator('item') except Exception, e: - logger.log(u"Error trying to load "+self.name+" RSS feed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to load "+self.name+" RSS feed: "+ex(e), logger.ERROR) logger.log(u"RSS data: "+data, logger.DEBUG) return [] @@ -236,7 +242,7 @@ class NewznabCache(tvcache.TVCache): try: responseSoup = etree.ElementTree(etree.XML(data)) - except Exception, e: + except Exception: return True if responseSoup.getroot().tag == 'error': diff --git a/sickbeard/providers/nzbmatrix.py b/sickbeard/providers/nzbmatrix.py index f74de9bfed8fd78649e4eaac62bfbd2577c69b12..316f6cad48f1e146476e718fdafc0f41ab3ed723 100644 --- a/sickbeard/providers/nzbmatrix.py +++ b/sickbeard/providers/nzbmatrix.py @@ -26,9 +26,9 @@ import xml.etree.cElementTree as etree import sickbeard import generic -from sickbeard import classes, logger, show_name_helpers, db +from sickbeard import classes, logger, show_name_helpers from sickbeard import tvcache -from sickbeard.common import * +from sickbeard.exceptions import ex class NZBMatrixProvider(generic.NZBProvider): @@ -94,7 +94,7 @@ class NZBMatrixProvider(generic.NZBProvider): responseSoup = etree.ElementTree(etree.XML(searchResult)) items = responseSoup.getiterator('item') except Exception, e: - logger.log(u"Error trying to load NZBMatrix RSS feed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to load NZBMatrix RSS feed: "+ex(e), logger.ERROR) return [] results = [] diff --git a/sickbeard/providers/nzbs_org.py b/sickbeard/providers/nzbs_org.py index b390c2eed178defb7cf6ebf6360ab6d26757ecfe..7e5b5ebe64f79eb7176af1cb5e6e4efc3c65dc4a 100644 --- a/sickbeard/providers/nzbs_org.py +++ b/sickbeard/providers/nzbs_org.py @@ -18,9 +18,10 @@ -import urllib import datetime +import re import time +import urllib import xml.etree.cElementTree as etree @@ -29,9 +30,9 @@ import generic from sickbeard import classes, show_name_helpers -from sickbeard import exceptions, logger, db -from sickbeard.common import * +from sickbeard import exceptions, logger from sickbeard import tvcache +from sickbeard.exceptions import ex class NZBsProvider(generic.NZBProvider): @@ -87,7 +88,7 @@ class NZBsProvider(generic.NZBProvider): responseSoup = etree.ElementTree(etree.XML(data)) items = responseSoup.getiterator('item') except Exception, e: - logger.log(u"Error trying to load NZBs.org RSS feed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to load NZBs.org RSS feed: "+ex(e), logger.ERROR) return [] results = [] diff --git a/sickbeard/providers/nzbsrus.py b/sickbeard/providers/nzbsrus.py index 26c5eb712aae0f0b7a0a397dfbfea1a0acfe0ee7..c90365f30768bd1d576459f6f440f772664ea85c 100644 --- a/sickbeard/providers/nzbsrus.py +++ b/sickbeard/providers/nzbsrus.py @@ -24,7 +24,6 @@ import sickbeard from sickbeard import exceptions, logger -from sickbeard.common import * from sickbeard import tvcache import generic diff --git a/sickbeard/providers/tvbinz.py b/sickbeard/providers/tvbinz.py deleted file mode 100644 index 6cb035851d01996a9db4d5fff56ddfb0d180073b..0000000000000000000000000000000000000000 --- a/sickbeard/providers/tvbinz.py +++ /dev/null @@ -1,120 +0,0 @@ -# Author: Nic Wolfe <nic@wolfeden.ca> -# URL: http://code.google.com/p/sickbeard/ -# -# This file is part of Sick Beard. -# -# Sick Beard is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Sick Beard is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import with_statement - -import urllib -import urllib2 -import sys - - -import sickbeard -import generic - -from sickbeard import helpers, classes, exceptions, logger -from sickbeard import tvcache - -from sickbeard.common import * - -class TVBinzProvider(generic.NZBProvider): - - def __init__(self): - - generic.NZBProvider.__init__(self, "TVBinz") - - self.cache = TVBinzCache(self) - - self.url = 'https://www.tvbinz.net/' - - def isEnabled(self): - return sickbeard.TVBINZ - - def _checkAuth(self): - if sickbeard.TVBINZ_UID in (None, "") or sickbeard.TVBINZ_HASH in (None, "") or sickbeard.TVBINZ_AUTH in (None, ""): - raise exceptions.AuthException("TVBinz authentication details are empty, check your config") - - def getURL (self, url): - - cookie_header = ("Cookie", "uid=" + sickbeard.TVBINZ_UID + ";hash=" + sickbeard.TVBINZ_HASH + ";auth=" + sickbeard.TVBINZ_AUTH) - - result = generic.NZBProvider.getURL(self, url, [cookie_header]) - - return result - - - -class TVBinzCache(tvcache.TVCache): - - def __init__(self, provider): - - tvcache.TVCache.__init__(self, provider) - - # only poll TVBinz every 10 minutes max - self.minTime = 10 - - def _getRSSData(self): - # get all records since the last timestamp - url = self.provider.url + "rss.php?" - - urlArgs = {'normalize': 1012, - 'n': 100, - 'maxage': sickbeard.USENET_RETENTION, - 'seriesinfo': 1, - 'nodupes': 1, - 'sets': 'none', - 'addauth': '1'} - - url += urllib.urlencode(urlArgs) - - logger.log(u"TVBinz cache update URL: "+ url, logger.DEBUG) - - data = self.provider.getURL(url) - - return data - - def _parseItem(self, item): - - if item.findtext('title') != None and item.findtext('title') == "You must be logged in to view this feed": - raise exceptions.AuthException("TVBinz authentication details are incorrect, check your config") - - if item.findtext('title') == None or item.findtext('link') == None: - logger.log(u"The XML returned from the TVBinz RSS feed is incomplete, this result is unusable: "+str(item), logger.ERROR) - return - - title = item.findtext('title') - url = item.findtext('link').replace('&', '&') - - sInfo = item.find('{http://tvbinz.net/rss/tvb/}seriesInfo') - if sInfo == None: - logger.log(u"No series info, this is some kind of non-standard release, ignoring it", logger.DEBUG) - return - - logger.log(u"Adding item from RSS to cache: "+title, logger.DEBUG) - - quality = Quality.nameQuality(title) - - if sInfo.findtext('{http://tvbinz.net/rss/tvb/}tvrID') == None: - tvrid = 0 - else: - tvrid = int(sInfo.findtext('{http://tvbinz.net/rss/tvb/}tvrID')) - - # since TVBinz normalizes the scene names it's more reliable to parse the episodes out myself - # than to rely on it, because it doesn't support multi-episode numbers in the feed - self._addCacheEntry(title, url, tvrage_id=tvrid, quality=quality) - -provider = TVBinzProvider() \ No newline at end of file diff --git a/sickbeard/providers/tvtorrents.py b/sickbeard/providers/tvtorrents.py index 5281a9a37531ff06fc7d599873f3419225095696..fa5e24a2d0106e39588b518681367c591ae897b6 100644 --- a/sickbeard/providers/tvtorrents.py +++ b/sickbeard/providers/tvtorrents.py @@ -16,14 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import urllib - import xml.etree.cElementTree as etree import sickbeard import generic -from sickbeard.common import * from sickbeard import logger from sickbeard import tvcache diff --git a/sickbeard/providers/womble.py b/sickbeard/providers/womble.py index b65b067e8349a4e2a08d9ae9f0e3e0ab2e377059..0591b58fdc8c65cca2ca35c90ab51c82af156565 100644 --- a/sickbeard/providers/womble.py +++ b/sickbeard/providers/womble.py @@ -18,13 +18,10 @@ -import urllib - import sickbeard -from sickbeard import exceptions, logger +from sickbeard import logger -from sickbeard.common import * from sickbeard import tvcache import generic diff --git a/sickbeard/sab.py b/sickbeard/sab.py index 77fa475189c117020647d13dba90148cf99f947d..80515ed9038ffb7af286c686c2d7b774035cc0c6 100644 --- a/sickbeard/sab.py +++ b/sickbeard/sab.py @@ -26,8 +26,9 @@ import sickbeard from lib import MultipartPostHandler import urllib2, cookielib -from sickbeard.common import * -from sickbeard import logger, classes +from sickbeard.common import USER_AGENT +from sickbeard import logger +from sickbeard.exceptions import ex def sendNZB(nzb): @@ -88,11 +89,11 @@ def sendNZB(nzb): f = opener.open(req) except (EOFError, IOError), e: - logger.log(u"Unable to connect to SAB: "+str(e), logger.ERROR) + logger.log(u"Unable to connect to SAB: "+ex(e), logger.ERROR) return False except httplib.InvalidURL, e: - logger.log(u"Invalid SAB host, check your config: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Invalid SAB host, check your config: "+ex(e), logger.ERROR) return False if f == None: @@ -102,7 +103,7 @@ def sendNZB(nzb): try: result = f.readlines() except Exception, e: - logger.log(u"Error trying to get result from SAB, NZB not sent: " + str(e), logger.ERROR) + logger.log(u"Error trying to get result from SAB, NZB not sent: " + ex(e), logger.ERROR) return False if len(result) == 0: diff --git a/sickbeard/scene_exceptions.py b/sickbeard/scene_exceptions.py index 999f869dd176c03528bffe81c7cf2373a7b2cd47..4fa8881654d8d97e6292be2ef31085f1b441c2a2 100644 --- a/sickbeard/scene_exceptions.py +++ b/sickbeard/scene_exceptions.py @@ -72,7 +72,7 @@ def retrieve_exceptions(): # each exception is on one line with the format tvdb_id: 'show name 1', 'show name 2', etc for cur_line in open_url.readlines(): - tvdb_id, sep, aliases = cur_line.partition(':') + tvdb_id, sep, aliases = cur_line.partition(':') #@UnusedVariable if not aliases: continue @@ -85,12 +85,21 @@ def retrieve_exceptions(): exception_dict[tvdb_id] = alias_list myDB = db.DBConnection("cache.db") - myDB.action("DELETE FROM scene_exceptions WHERE 1=1") + + changed_exceptions = False # write all the exceptions we got off the net into the database for cur_tvdb_id in exception_dict: + + # get a list of the existing exceptions for this ID + existing_exceptions = [x["show_name"] for x in myDB.select("SELECT * FROM scene_exceptions WHERE tvdb_id = ?", [cur_tvdb_id])] + for cur_exception in exception_dict[cur_tvdb_id]: - myDB.action("INSERT INTO scene_exceptions (tvdb_id, show_name) VALUES (?,?)", [cur_tvdb_id, cur_exception]) + # if this exception isn't already in the DB then add it + if cur_exception not in existing_exceptions: + myDB.action("INSERT INTO scene_exceptions (tvdb_id, show_name) VALUES (?,?)", [cur_tvdb_id, cur_exception]) + changed_exceptions = True # since this could invalidate the results of the cache we clear it out after updating - name_cache.clearCache() \ No newline at end of file + if changed_exceptions: + name_cache.clearCache() \ No newline at end of file diff --git a/sickbeard/scheduler.py b/sickbeard/scheduler.py index 55b6deefea3dbee7731ae724e6ef1f56dbea7ab2..6b31a71297998eb144b8a20523a6b2d6347f1af4 100644 --- a/sickbeard/scheduler.py +++ b/sickbeard/scheduler.py @@ -21,9 +21,8 @@ import time import threading import traceback -from lib.tvdb_api import tvdb_exceptions - from sickbeard import logger +from sickbeard.exceptions import ex class Scheduler: @@ -71,7 +70,7 @@ class Scheduler: logger.log(u"Starting new thread: "+self.threadName, logger.DEBUG) self.action.run() except Exception, e: - logger.log(u"Exception generated in thread "+self.threadName+": " + str(e), logger.ERROR) + logger.log(u"Exception generated in thread "+self.threadName+": " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) if self.abort: diff --git a/sickbeard/search.py b/sickbeard/search.py index f4a1726ee13ea3d713b8537a23a0f39de20d0ad2..0175e5801c3cfb4482e736014e4d3c377443ee6b 100644 --- a/sickbeard/search.py +++ b/sickbeard/search.py @@ -18,11 +18,12 @@ from __future__ import with_statement +import os import traceback import sickbeard -from common import * +from common import SNATCHED, Quality, SEASON_RESULT, MULTI_EP_RESULT from sickbeard import logger, db, show_name_helpers, exceptions, helpers from sickbeard import sab @@ -30,10 +31,9 @@ from sickbeard import nzbget from sickbeard import history from sickbeard import notifiers from sickbeard import nzbSplitter - +from sickbeard import ui from sickbeard import encodingKludge as ek - -from sickbeard.providers import * +from sickbeard.exceptions import ex from sickbeard import providers def _downloadResult(result): @@ -74,7 +74,7 @@ def _downloadResult(result): fileOut.close() helpers.chmodAsParent(fileName) except IOError, e: - logger.log(u"Error trying to save NZB to black hole: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to save NZB to black hole: "+ex(e), logger.ERROR) newResult = False elif resProvider.providerType == "torrent": @@ -84,6 +84,9 @@ def _downloadResult(result): logger.log(u"Invalid provider type - this is a coding error, report it please", logger.ERROR) return False + if newResult: + ui.notifications.message('Episode <b>%s</b> snatched from <b>%s</b>' % (result.name, resProvider.name)) + return newResult def snatchEpisode(result, endStatus=SNATCHED): @@ -151,10 +154,10 @@ def searchForNeededEpisodes(): try: curFoundResults = curProvider.searchRSS() except exceptions.AuthException, e: - logger.log(u"Authentication error: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Authentication error: "+ex(e), logger.ERROR) continue except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) continue @@ -232,10 +235,10 @@ def findEpisode(episode, manualSearch=False): try: curFoundResults = curProvider.findEpisode(episode, manualSearch=manualSearch) except exceptions.AuthException, e: - logger.log(u"Authentication error: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Authentication error: "+ex(e), logger.ERROR) continue except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) continue @@ -281,10 +284,10 @@ def findSeason(show, season): foundResults[curEp] = curResults[curEp] except exceptions.AuthException, e: - logger.log(u"Authentication error: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Authentication error: "+ex(e), logger.ERROR) continue except Exception, e: - logger.log(u"Error while searching "+curProvider.name+", skipping: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error while searching "+curProvider.name+", skipping: "+ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) continue diff --git a/sickbeard/searchBacklog.py b/sickbeard/searchBacklog.py index 11e2bbf83a552b49dabdc16738bb49e4318d35aa..ce92424a063882c3516ad39f8f367f6a38839da0 100644 --- a/sickbeard/searchBacklog.py +++ b/sickbeard/searchBacklog.py @@ -20,13 +20,14 @@ from __future__ import with_statement import datetime import threading -import time -from sickbeard import db, exceptions, helpers, search, scheduler +import sickbeard + +from sickbeard import db, scheduler from sickbeard import search_queue from sickbeard import logger from sickbeard import ui -from sickbeard.common import * +#from sickbeard.common import * class BacklogSearchScheduler(scheduler.Scheduler): @@ -90,11 +91,11 @@ class BacklogSearcher: self.amActive = True self.amPaused = False - myDB = db.DBConnection() - numSeasonResults = myDB.select("SELECT DISTINCT(season), showid FROM tv_episodes ep, tv_shows show WHERE season != 0 AND ep.showid = show.tvdb_id AND show.paused = 0 AND ep.airdate > ?", [fromDate.toordinal()]) + #myDB = db.DBConnection() + #numSeasonResults = myDB.select("SELECT DISTINCT(season), showid FROM tv_episodes ep, tv_shows show WHERE season != 0 AND ep.showid = show.tvdb_id AND show.paused = 0 AND ep.airdate > ?", [fromDate.toordinal()]) # get separate lists of the season/date shows - season_shows = [x for x in show_list if not x.air_by_date] + #season_shows = [x for x in show_list if not x.air_by_date] air_by_date_shows = [x for x in show_list if x.air_by_date] # figure out how many segments of air by date shows we're going to do @@ -104,8 +105,8 @@ class BacklogSearcher: logger.log(u"Air-by-date segments: "+str(air_by_date_segments), logger.DEBUG) - totalSeasons = float(len(numSeasonResults) + len(air_by_date_segments)) - numSeasonsDone = 0.0 + #totalSeasons = float(len(numSeasonResults) + len(air_by_date_segments)) + #numSeasonsDone = 0.0 # go through non air-by-date shows and see if they need any episodes for curShow in show_list: @@ -127,7 +128,7 @@ class BacklogSearcher: if not backlog_queue_item.wantSeason: logger.log(u"Nothing in season "+str(cur_segment)+" needs to be downloaded, skipping this season", logger.DEBUG) else: - sickbeard.searchQueueScheduler.action.add_item(backlog_queue_item) + sickbeard.searchQueueScheduler.action.add_item(backlog_queue_item) #@UndefinedVariable # don't consider this an actual backlog search if we only did recent eps # or if we only did certain shows diff --git a/sickbeard/searchCurrent.py b/sickbeard/searchCurrent.py index f28019039def7f4c638f3c13d8612b1afe5e7f9c..ff3392a759e0fb36a56d2491867d08a566eae19c 100644 --- a/sickbeard/searchCurrent.py +++ b/sickbeard/searchCurrent.py @@ -18,15 +18,11 @@ from __future__ import with_statement -from sickbeard import common, db, exceptions, helpers, search +import sickbeard + from sickbeard import search_queue -from sickbeard import logger -from sickbeard import ui -from sickbeard.common import * -import datetime import threading -import time class CurrentSearcher(): @@ -37,4 +33,4 @@ class CurrentSearcher(): def run(self): search_queue_item = search_queue.RSSSearchQueueItem() - sickbeard.searchQueueScheduler.action.add_item(search_queue_item) + sickbeard.searchQueueScheduler.action.add_item(search_queue_item) #@UndefinedVariable diff --git a/sickbeard/search_queue.py b/sickbeard/search_queue.py index da136ade643be0334740c89a9ed866b8882c8167..148aa1979e984cddaf683e108fad20cf8e785c97 100644 --- a/sickbeard/search_queue.py +++ b/sickbeard/search_queue.py @@ -25,9 +25,11 @@ import sickbeard from sickbeard import db, logger, common, exceptions, helpers from sickbeard import generic_queue from sickbeard import search +from sickbeard import ui BACKLOG_SEARCH = 10 RSS_SEARCH = 20 +MANUAL_SEARCH = 30 class SearchQueue(generic_queue.GenericQueue): @@ -41,6 +43,12 @@ class SearchQueue(generic_queue.GenericQueue): return True return False + def is_ep_in_queue(self, ep_obj): + for cur_item in self.queue: + if isinstance(cur_item, ManualSearchQueueItem) and cur_item.ep_obj == ep_obj: + return True + return False + def pause_backlog(self): self.min_priority = generic_queue.QueuePriorities.HIGH @@ -58,16 +66,58 @@ class SearchQueue(generic_queue.GenericQueue): return False def add_item(self, item): + if isinstance(item, RSSSearchQueueItem): + generic_queue.GenericQueue.add_item(self, item) # don't do duplicates - if isinstance(item, RSSSearchQueueItem) or not self.is_in_queue(item.show, item.segment): + elif isinstance(item, BacklogQueueItem) and not self.is_in_queue(item.show, item.segment): + generic_queue.GenericQueue.add_item(self, item) + elif isinstance(item, ManualSearchQueueItem) and not self.is_ep_in_queue(item.ep_obj): generic_queue.GenericQueue.add_item(self, item) else: logger.log(u"Not adding item, it's already in the queue", logger.DEBUG) +class ManualSearchQueueItem(generic_queue.QueueItem): + def __init__(self, ep_obj): + generic_queue.QueueItem.__init__(self, 'Manual Search', MANUAL_SEARCH) + self.priority = generic_queue.QueuePriorities.HIGH + + self.ep_obj = ep_obj + + self.success = None + + def execute(self): + generic_queue.QueueItem.execute(self) + + logger.log("Searching for download for " + self.ep_obj.prettyName(True)) + + foundEpisode = search.findEpisode(self.ep_obj, manualSearch=True) + + if not foundEpisode: + ui.notifications.message('No downloads were found', "Couldn't find a download for <i>%s</i>" % self.ep_obj.prettyName(True)) + logger.log(u"Unable to find a download for "+self.ep_obj.prettyName(True)) + + else: + + # just use the first result for now + logger.log(u"Downloading episode from " + foundEpisode.url) + result = search.snatchEpisode(foundEpisode) + providerModule = foundEpisode.provider + if not result: + ui.notifications.error('Error while attempting to snatch '+foundEpisode.name+', check your logs') + elif providerModule == None: + ui.notifications.error('Provider is configured incorrectly, unable to download') + + self.success = foundEpisode + + def finish(self): + # don't let this linger if something goes wrong + if self.success == None: + self.success = False + generic_queue.QueueItem.finish(self) + class RSSSearchQueueItem(generic_queue.QueueItem): def __init__(self): generic_queue.QueueItem.__init__(self, 'RSS Search', RSS_SEARCH) - self.priority = generic_queue.QueuePriorities.HIGH def execute(self): generic_queue.QueueItem.execute(self) @@ -119,6 +169,7 @@ class RSSSearchQueueItem(generic_queue.QueueItem): class BacklogQueueItem(generic_queue.QueueItem): def __init__(self, show, segment): generic_queue.QueueItem.__init__(self, 'Backlog', BACKLOG_SEARCH) + self.priority = generic_queue.QueuePriorities.LOW self.thread_name = 'BACKLOG-'+str(show.tvdbid) self.show = show @@ -144,7 +195,7 @@ class BacklogQueueItem(generic_queue.QueueItem): statusResults = myDB.select("SELECT status FROM tv_episodes WHERE showid = ? AND airdate >= ? AND airdate <= ?", [self.show.tvdbid, min_date.toordinal(), max_date.toordinal()]) - anyQualities, bestQualities = common.Quality.splitQuality(self.show.quality) + anyQualities, bestQualities = common.Quality.splitQuality(self.show.quality) #@UnusedVariable self.wantSeason = self._need_any_episodes(statusResults, bestQualities) def execute(self): diff --git a/sickbeard/showUpdater.py b/sickbeard/showUpdater.py index 5ffd5008874a32903bd5bf433ac8531366072887..bb0830ee3696d2e8c80d0e394a46033132ecb468 100644 --- a/sickbeard/showUpdater.py +++ b/sickbeard/showUpdater.py @@ -18,11 +18,12 @@ import datetime -from sickbeard.common import * +import sickbeard from sickbeard import logger from sickbeard import exceptions from sickbeard import ui +from sickbeard.exceptions import ex class ShowUpdater(): @@ -51,15 +52,15 @@ class ShowUpdater(): try: if curShow.status != "Ended": - curQueueItem = sickbeard.showQueueScheduler.action.updateShow(curShow, True) + curQueueItem = sickbeard.showQueueScheduler.action.updateShow(curShow, True) #@UndefinedVariable else: #TODO: maybe I should still update specials? logger.log(u"Not updating episodes for show "+curShow.name+" because it's marked as ended.", logger.DEBUG) - curQueueItem = sickbeard.showQueueScheduler.action.refreshShow(curShow, True) + curQueueItem = sickbeard.showQueueScheduler.action.refreshShow(curShow, True) #@UndefinedVariable piList.append(curQueueItem) except (exceptions.CantUpdateException, exceptions.CantRefreshException), e: - logger.log(u"Automatic update failed: " + str(e), logger.ERROR) + logger.log(u"Automatic update failed: " + ex(e), logger.ERROR) ui.ProgressIndicators.setIndicator('dailyUpdate', ui.QueueProgressIndicator("Daily Update", piList)) diff --git a/sickbeard/show_name_helpers.py b/sickbeard/show_name_helpers.py index 7b7944c70210270eef883cb5b5855a44b685ec06..dc244dad15f98ce5b31b9fcc0bc895c56beeca03 100644 --- a/sickbeard/show_name_helpers.py +++ b/sickbeard/show_name_helpers.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. +import sickbeard + from sickbeard.common import countryList from sickbeard.helpers import sanitizeSceneName from sickbeard.scene_exceptions import get_scene_exceptions @@ -24,14 +26,12 @@ from sickbeard import db import re import datetime -import urllib from name_parser.parser import NameParser, InvalidNameException -resultFilters = ("sub(pack|s|bed)", "nlsub(bed|s)?", "swesub(bed)?", +resultFilters = ["sub(pack|s|bed)", "nlsub(bed|s)?", "swesub(bed)?", "(dir|sample|nfo)fix", "sample", "(dvd)?extras", - "dub(bed)?", "german", "french", "core2hd", - "dutch", "swedish") + "dub(bed)?"] def filterBadReleases(name): """ @@ -50,13 +50,23 @@ def filterBadReleases(name): logger.log(u"Unable to parse the filename "+name+" into a valid episode", logger.WARNING) return False + # use the extra info and the scene group to filter against + check_string = '' + if parse_result.extra_info: + check_string = parse_result.extra_info + if parse_result.release_group: + if check_string: + check_string = check_string + '-' + parse_result.release_group + else: + check_string = parse_result.release_group + # if there's no info after the season info then assume it's fine - if not parse_result.extra_info: + if not check_string: return True # if any of the bad strings are in the name then say no - for x in resultFilters: - if re.search('(^|[\W_])'+x+'($|[\W_])', parse_result.extra_info, re.I): + for x in resultFilters + sickbeard.IGNORE_WORDS.split(','): + if re.search('(^|[\W_])'+x+'($|[\W_])', check_string, re.I): logger.log(u"Invalid scene release: "+name+" contains "+x+", ignoring it", logger.DEBUG) return False diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py index 1ff65aa50043c459fe631e90037b61ecdfd15001..cbfa169fda94374bc1a16d305525d420bd979b30 100644 --- a/sickbeard/show_queue.py +++ b/sickbeard/show_queue.py @@ -18,18 +18,19 @@ from __future__ import with_statement -import os.path -import threading import traceback +import sickbeard + from lib.tvdb_api import tvdb_exceptions, tvdb_api -from sickbeard.common import * +from sickbeard.common import SKIPPED, WANTED from sickbeard.tv import TVShow -from sickbeard import exceptions, helpers, logger, ui, db +from sickbeard import exceptions, logger, ui, db from sickbeard import generic_queue from sickbeard import name_cache +from sickbeard.exceptions import ex class ShowQueue(generic_queue.GenericQueue): @@ -87,7 +88,7 @@ class ShowQueue(generic_queue.GenericQueue): else: queueItemObj = QueueItemForceUpdate(show) - self.queue.append(queueItemObj) + self.add_item(queueItemObj) return queueItemObj @@ -102,7 +103,7 @@ class ShowQueue(generic_queue.GenericQueue): queueItemObj = QueueItemRefresh(show) - self.queue.append(queueItemObj) + self.add_item(queueItemObj) return queueItemObj @@ -110,13 +111,14 @@ class ShowQueue(generic_queue.GenericQueue): queueItemObj = QueueItemRename(show) - self.queue.append(queueItemObj) + self.add_item(queueItemObj) return queueItemObj def addShow(self, tvdb_id, showDir, default_status=None, quality=None, season_folders=None, lang="en"): queueItemObj = QueueItemAdd(tvdb_id, showDir, default_status, quality, season_folders, lang) - self.queue.append(queueItemObj) + + self.add_item(queueItemObj) return queueItemObj @@ -149,7 +151,7 @@ class ShowQueueItem(generic_queue.QueueItem): self.show = show def isInQueue(self): - return self in sickbeard.showQueueScheduler.action.queue+[sickbeard.showQueueScheduler.action.currentItem] + return self in sickbeard.showQueueScheduler.action.queue+[sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable def _getName(self): return str(self.show.tvdbid) @@ -220,12 +222,12 @@ class QueueItemAdd(ShowQueueItem): # this usually only happens if they have an NFO in their show dir which gave us a TVDB ID that has no # proper english version of the show if not s or not s['seriesname']: - ui.flash.error("Unable to add show", "Show in "+self.showDir+" has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") + ui.notifications.error("Unable to add show", "Show in "+self.showDir+" has no name on TVDB, probably the wrong language. Delete .nfo and add manually in the correct language.") self._finishEarly() return except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Error contacting TVDB: "+str(e), logger.ERROR) - ui.flash.error("Unable to add show", "Unable to look up the show in "+self.showDir+" on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") + logger.log(u"Error contacting TVDB: "+ex(e), logger.ERROR) + ui.notifications.error("Unable to add show", "Unable to look up the show in "+self.showDir+" on TVDB, not using the NFO. Delete .nfo and add manually in the correct language.") self._finishEarly() return @@ -248,22 +250,22 @@ class QueueItemAdd(ShowQueueItem): self.show.air_by_date = 1 except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to add show due to an error with TVDB: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to add show due to an error with TVDB: "+ex(e), logger.ERROR) if self.show: - ui.flash.error("Unable to add "+str(self.show.name)+" due to an error with TVDB") + ui.notifications.error("Unable to add "+str(self.show.name)+" due to an error with TVDB") else: - ui.flash.error("Unable to add show due to an error with TVDB") + ui.notifications.error("Unable to add show due to an error with TVDB") self._finishEarly() return except exceptions.MultipleShowObjectsException: logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) - ui.flash.error('Show skipped', "The show in " + self.showDir + " is already in your show list") + ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") self._finishEarly() return except Exception, e: - logger.log(u"Error trying to add show: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to add show: "+ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) self._finishEarly() raise @@ -274,7 +276,7 @@ class QueueItemAdd(ShowQueueItem): try: self.show.loadEpisodesFromDir() except Exception, e: - logger.log(u"Error searching dir for episodes: " + str(e), logger.ERROR) + logger.log(u"Error searching dir for episodes: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) try: @@ -285,13 +287,13 @@ class QueueItemAdd(ShowQueueItem): self.show.populateCache() except Exception, e: - logger.log(u"Error with TVDB, not creating episode list: " + str(e), logger.ERROR) + logger.log(u"Error with TVDB, not creating episode list: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) try: self.show.saveToDB() except Exception, e: - logger.log(u"Error saving the episode to the database: " + str(e), logger.ERROR) + logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) # if they gave a custom status then change all the eps to it @@ -303,7 +305,7 @@ class QueueItemAdd(ShowQueueItem): # if they started with WANTED eps then run the backlog if self.default_status == WANTED: logger.log(u"Launching backlog for this show since its episodes are WANTED") - sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) + sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable self.show.flushEpisodes() @@ -361,7 +363,11 @@ class QueueItemUpdate(ShowQueueItem): logger.log(u"Beginning update of "+self.show.name) logger.log(u"Retrieving show info from TVDB", logger.DEBUG) - self.show.loadFromTVDB(cache=not self.force) + try: + self.show.loadFromTVDB(cache=not self.force) + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB, aborting: "+ex(e), logger.WARNING) + return # get episode list from DB logger.log(u"Loading all episodes from the database", logger.DEBUG) @@ -372,7 +378,7 @@ class QueueItemUpdate(ShowQueueItem): try: TVDBEpList = self.show.loadEpisodesFromTVDB(cache=not self.force) except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to get info from TVDB, the show info will not be refreshed: "+ex(e), logger.ERROR) TVDBEpList = None if TVDBEpList == None: @@ -404,7 +410,7 @@ class QueueItemUpdate(ShowQueueItem): if self.show.tvrid == 0: self.show.setTVRID() - sickbeard.showQueueScheduler.action.refreshShow(self.show, True) + sickbeard.showQueueScheduler.action.refreshShow(self.show, True) #@UndefinedVariable class QueueItemForceUpdate(QueueItemUpdate): def __init__(self, show=None): diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 885b7428191b62bc43451fad59428c497c4f3639..49d06d6ca4988d03d0b96e643319849ac72c905e 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -34,6 +34,7 @@ from lib.tvdb_api import tvdb_api, tvdb_exceptions from sickbeard import db from sickbeard import helpers, exceptions, logger +from sickbeard.exceptions import ex from sickbeard import tvrage from sickbeard import config from sickbeard import image_cache @@ -41,7 +42,8 @@ from sickbeard import postProcessor from sickbeard import encodingKludge as ek -from common import * +from common import Quality, Overview +from common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, ARCHIVED, IGNORED, UNAIRED, WANTED, SKIPPED, UNKNOWN class TVShow(object): @@ -70,7 +72,7 @@ class TVShow(object): self._isDirGood = False self.episodes = {} - + otherShow = helpers.findCertainShow(sickbeard.showList, self.tvdbid) if otherShow != None: raise exceptions.MultipleShowObjectsException("Can't create a show if it already exists") @@ -198,7 +200,7 @@ class TVShow(object): try: curEpisode = self.makeEpFromFile(os.path.join(self._location, mediaFile)) except (exceptions.ShowNotFoundException, exceptions.EpisodeNotFoundException), e: - logger.log(u"Episode "+mediaFile+" returned an exception: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Episode "+mediaFile+" returned an exception: "+ex(e), logger.ERROR) except exceptions.EpisodeDeletedException: logger.log(u"The episode deleted itself when I tried making an object for it", logger.DEBUG) @@ -313,10 +315,10 @@ class TVShow(object): try: # load the tvrage object, it will set the ID in its constructor if possible - tvr = tvrage.TVRage(self) + tvrage.TVRage(self) self.saveToDB() except exceptions.TVRageException, e: - logger.log(u"Couldn't get TVRage ID because we're unable to sync TVDB and TVRage: "+str(e).decode('utf-8'), logger.DEBUG) + logger.log(u"Couldn't get TVRage ID because we're unable to sync TVDB and TVRage: "+ex(e), logger.DEBUG) return def getImages(self, fanart=None, poster=None): @@ -345,7 +347,7 @@ class TVShow(object): # make an episode out of it except exceptions.TVRageException, e: - logger.log(u"Unable to add TVRage info: " + str(e), logger.WARNING) + logger.log(u"Unable to add TVRage info: " + ex(e), logger.WARNING) @@ -390,9 +392,12 @@ class TVShow(object): epObj = t[self.tvdbid].airedOn(parse_result.air_date)[0] season = int(epObj["seasonnumber"]) episodes = [int(epObj["episodenumber"])] - except tvdb_exceptions.tvdb_episodenotfound, e: + except tvdb_exceptions.tvdb_episodenotfound: logger.log(u"Unable to find episode with date "+str(episodes[0])+" for show "+self.name+", skipping", logger.WARNING) return None + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) + return None for curEpNum in episodes: @@ -538,6 +543,7 @@ class TVShow(object): ltvdb_api_parms['language'] = self.lang t = tvdb_api.Tvdb(**ltvdb_api_parms) + else: t = tvapi @@ -595,14 +601,14 @@ class TVShow(object): raise exceptions.NoNFOException("Empty <id> or <tvdbid> field in NFO") except (exceptions.NoNFOException, SyntaxError, ValueError), e: - logger.log(u"There was an error parsing your existing tvshow.nfo file: " + str(e), logger.ERROR) + logger.log(u"There was an error parsing your existing tvshow.nfo file: " + ex(e), logger.ERROR) logger.log(u"Attempting to rename it to tvshow.nfo.old", logger.DEBUG) try: xmlFileObj.close() ek.ek(os.rename, xmlFile, xmlFile + ".old") except Exception, e: - logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + str(e), logger.ERROR) + logger.log(u"Failed to rename your tvshow.nfo file - you need to delete it or fix it: " + ex(e), logger.ERROR) raise exceptions.NoNFOException("Invalid info in tvshow.nfo") if showXML.findtext('studio') != None: @@ -753,7 +759,6 @@ class TVShow(object): goodName = rootEp.prettyName() actualName = os.path.splitext(os.path.basename(curLocation)) - curEpDir = os.path.dirname(curLocation) if goodName == actualName[0]: logger.log(str(self.tvdbid) + ": File " + rootEp.location + " is already named correctly, skipping", logger.DEBUG) @@ -890,7 +895,7 @@ class TVShow(object): return Overview.GOOD elif epStatus in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER: - anyQualities, bestQualities = Quality.splitQuality(self.quality) + anyQualities, bestQualities = Quality.splitQuality(self.quality) #@UnusedVariable if bestQualities: maxBestQuality = max(bestQualities) else: @@ -1078,7 +1083,7 @@ class TVEpisode(object): myEp = cachedSeason[episode] except (tvdb_exceptions.tvdb_error, IOError), e: - logger.log(u"TVDB threw up an error: "+str(e).decode('utf-8'), logger.DEBUG) + logger.log(u"TVDB threw up an error: "+ex(e), logger.DEBUG) # if the episode is already valid just log it, if not throw it up if self.name: logger.log(u"TVDB timed out but we have enough info from other sources, allowing the error", logger.DEBUG) @@ -1198,11 +1203,11 @@ class TVEpisode(object): try: showXML = etree.ElementTree(file = nfoFile) except (SyntaxError, ValueError), e: - logger.log(u"Error loading the NFO, backing up the NFO and skipping for now: " + str(e), logger.ERROR) #TODO: figure out what's wrong and fix it + logger.log(u"Error loading the NFO, backing up the NFO and skipping for now: " + ex(e), logger.ERROR) #TODO: figure out what's wrong and fix it try: ek.ek(os.rename, nfoFile, nfoFile + ".old") except Exception, e: - logger.log(u"Failed to rename your episode's NFO file - you need to delete it or fix it: " + str(e), logger.ERROR) + logger.log(u"Failed to rename your episode's NFO file - you need to delete it or fix it: " + ex(e), logger.ERROR) raise exceptions.NoNFOException("Error in NFO format") for epDetails in showXML.getiterator('episodedetails'): @@ -1420,7 +1425,7 @@ class TVEpisode(object): finalName += goodName if naming_quality: - epStatus, epQual = Quality.splitCompositeStatus(self.status) + epStatus, epQual = Quality.splitCompositeStatus(self.status) #@UnusedVariable if epQual != Quality.NONE: finalName += config.naming_sep_type[naming_sep_type] + Quality.qualityStrings[epQual] diff --git a/sickbeard/tvcache.py b/sickbeard/tvcache.py index f2b1fae485c8a678fc1caa0dedc05f24d5fbeac7..75a154838fc51db73b90ec536a0a54e30b7dae23 100644 --- a/sickbeard/tvcache.py +++ b/sickbeard/tvcache.py @@ -24,11 +24,11 @@ import sickbeard from sickbeard import db from sickbeard import logger -from sickbeard.common import * +from sickbeard.common import Quality -from sickbeard import helpers, classes, exceptions, show_name_helpers -from sickbeard import providers +from sickbeard import helpers, exceptions, show_name_helpers from sickbeard import name_cache +from sickbeard.exceptions import ex import xml.etree.cElementTree as etree @@ -114,7 +114,7 @@ class TVCache(): responseSoup = etree.ElementTree(etree.XML(data)) items = responseSoup.getiterator('item') except Exception, e: - logger.log(u"Error trying to load "+self.provider.name+" RSS feed: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error trying to load "+self.provider.name+" RSS feed: "+ex(e), logger.ERROR) logger.log(u"Feed contents: "+repr(data), logger.DEBUG) return [] @@ -200,6 +200,8 @@ class TVCache(): logger.log(u"No series name retrieved from "+name+", unable to cache it", logger.DEBUG) return False + tvdb_lang = None + # if we need tvdb_id or tvrage_id then search the DB for them if not tvdb_id or not tvrage_id: @@ -291,9 +293,12 @@ class TVCache(): epObj = t[tvdb_id].airedOn(parse_result.air_date)[0] season = int(epObj["seasonnumber"]) episodes = [int(epObj["episodenumber"])] - except tvdb_exceptions.tvdb_episodenotfound, e: + except tvdb_exceptions.tvdb_episodenotfound: logger.log(u"Unable to find episode with date "+str(parse_result.air_date)+" for show "+parse_result.series_name+", skipping", logger.WARNING) return False + except tvdb_exceptions.tvdb_error, e: + logger.log(u"Unable to contact TVDB: "+ex(e), logger.WARNING) + return False episodeText = "|"+"|".join(map(str, episodes))+"|" diff --git a/sickbeard/tvrage.py b/sickbeard/tvrage.py index 91aaff411162effeb7646121982126ebd19e26a2..040f425e5e81389e1b6397e0a0e7f5b429581b42 100644 --- a/sickbeard/tvrage.py +++ b/sickbeard/tvrage.py @@ -25,10 +25,11 @@ import traceback import sickbeard from sickbeard import logger -from sickbeard.common import * +from sickbeard.common import UNAIRED from sickbeard import db from sickbeard import exceptions, helpers +from sickbeard.exceptions import ex from lib.tvdb_api import tvdb_api, tvdb_exceptions @@ -137,7 +138,7 @@ class TVRage: # if we couldn't compare with TVDB try comparing it with the local database except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to check TVRage info against TVDB: "+str(e).decode('utf-8')) + logger.log(u"Unable to check TVRage info against TVDB: "+ex(e)) logger.log(u"Trying against DB instead", logger.DEBUG) @@ -159,7 +160,7 @@ class TVRage: logger.log(u"Date from TVRage for episode " + str(curSeason) + "x1: " + str(curEpInfo['airdate']), logger.DEBUG) except Exception, e: - logger.log(u"Error encountered while checking TVRage<->TVDB sync: " + str(e), logger.WARNING) + logger.log(u"Error encountered while checking TVRage<->TVDB sync: " + ex(e), logger.WARNING) logger.log(traceback.format_exc(), logger.DEBUG) return False @@ -198,7 +199,7 @@ class TVRage: airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2]) except tvdb_exceptions.tvdb_exception, e: - logger.log(u"Unable to check TVRage info against TVDB: "+str(e).decode('utf-8')) + logger.log(u"Unable to check TVRage info against TVDB: "+ex(e)) logger.log(u"Trying against DB instead", logger.DEBUG) @@ -217,7 +218,7 @@ class TVRage: return True except Exception, e: - logger.log(u"Error encountered while checking TVRage<->TVDB sync: " + str(e), logger.WARNING) + logger.log(u"Error encountered while checking TVRage<->TVDB sync: " + ex(e), logger.WARNING) logger.log(traceback.format_exc(), logger.DEBUG) return False @@ -250,7 +251,7 @@ class TVRage: try: result = helpers.getURL(url).decode('utf-8') except (urllib2.HTTPError, IOError), e: - logger.log(u"Unable to load TVRage info: " + str(e)) + logger.log(u"Unable to load TVRage info: " + ex(e)) raise exceptions.TVRageException("urlopen call to " + url + " failed") urlData = result.splitlines() @@ -296,10 +297,10 @@ class TVRage: try: date = datetime.datetime.strptime(epInfo[2], "%b/%d/%Y").date() - except ValueError, e: + except ValueError: try: date = datetime.datetime.strptime(epInfo[2], "%d/%b/%Y").date() - except ValueError, e: + except ValueError: logger.log(u"Unable to figure out the time from the TVRage data "+epInfo[2]) return None @@ -335,6 +336,6 @@ class TVRage: try: ep = self.show.getEpisode(self.nextEpInfo['season'], self.nextEpInfo['episode']) except exceptions.SickBeardException, e: - logger.log(u"Unable to create episode from tvrage (could be for a variety of reasons): " + str(e)) + logger.log(u"Unable to create episode from tvrage (could be for a variety of reasons): " + ex(e)) return ep diff --git a/sickbeard/ui.py b/sickbeard/ui.py index 2cb39305aa570520234ee43c5e7a0ca96bd4e18f..256ce9809f0dc1a180e4dfcc3ec0d24529bfebfb 100644 --- a/sickbeard/ui.py +++ b/sickbeard/ui.py @@ -16,18 +16,99 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. - - -import os.path import datetime +import cherrypy import sickbeard -from sickbeard import exceptions -from sickbeard.tv import TVShow -from sickbeard import logger -from sickbeard import classes +MESSAGE = 'notice' +ERROR = 'error' -from lib.tvdb_api import tvdb_exceptions +class Notifications(object): + """ + A queue of Notification objects. + """ + def __init__(self): + self._messages = [] + self._errors = [] + + def message(self, title, message=''): + """ + Add a regular notification to the queue + + title: The title of the notification + message: The message portion of the notification + """ + self._messages.append(Notification(title, message, MESSAGE)) + + def error(self, title, message=''): + """ + Add an error notification to the queue + + title: The title of the notification + message: The message portion of the notification + """ + self._errors.append(Notification(title, message, ERROR)) + + def get_notifications(self): + """ + Return all the available notifications in a list. Marks them all as seen + as it returns them. Also removes timed out Notifications from the queue. + + Returns: A list of Notification objects + """ + + # filter out expired notifications + self._errors = [x for x in self._errors if not x.is_expired()] + self._messages = [x for x in self._messages if not x.is_expired()] + + # return any notifications that haven't been shown to the client already + return [x.see() for x in self._errors + self._messages if x.is_new()] + +# static notification queue object +notifications = Notifications() + + +class Notification(object): + """ + Represents a single notification. Tracks its own timeout and a list of which clients have + seen it before. + """ + def __init__(self, title, message='', type=None, timeout=None): + self.title = title + self.message = message + + self._when = datetime.datetime.now() + self._seen = [] + + if type: + self.type = type + else: + self.type = MESSAGE + + if timeout: + self._timeout = timeout + else: + self._timeout = datetime.timedelta(minutes=1) + + def is_new(self): + """ + Returns True if the notification hasn't been displayed to the current client (aka IP address). + """ + return cherrypy.request.remote.ip not in self._seen + + def is_expired(self): + """ + Returns True if the notification is older than the specified timeout value. + """ + return datetime.datetime.now() - self._when > self._timeout + + + def see(self): + """ + Returns this notification object and marks it as seen by the client ip + """ + self._seen.append(cherrypy.request.remote.ip) + return self class ProgressIndicator(): @@ -76,7 +157,7 @@ class QueueProgressIndicator(): return len([x for x in self.queueItemList if x.isInQueue()]) def nextName(self): - for curItem in [sickbeard.showQueueScheduler.action.currentItem]+sickbeard.showQueueScheduler.action.queue: + for curItem in [sickbeard.showQueueScheduler.action.currentItem]+sickbeard.showQueueScheduler.action.queue: #@UndefinedVariable if curItem in self.queueItemList: return curItem.name @@ -96,57 +177,4 @@ class LoadingTVShow(): self.dir = dir self.show = None -def addShowsFromRootDir(dir): - - returnStr = "" - - if not os.path.isdir(dir): - return "Couldn't find directory " + dir - - for curDir in os.listdir(unicode(dir)): - showDir = os.path.join(dir, curDir) - logStr = "Attempting to load show in " + showDir - logger.log(logStr, logger.DEBUG) - returnStr += logStr + "<br />\n" - - sickbeard.loadingShowList[showDir] = LoadingTVShow(showDir) - - try: - #myAdder = ShowAdder(showDir) - #myAdder.start() - sickbeard.showAddScheduler.action.addShowToQueue(showDir) - except exceptions.NoNFOException: - logStr = "Unable to automatically add the show in " + showDir - logger.log(logStr, logger.ERROR) - returnStr += logStr + "<br />\n" - del sickbeard.loadingShowList[showDir] - except exceptions.MultipleShowObjectsException: - logStr = "Show in "+showDir+" already exists, skipping..." - logger.log(logStr, logger.ERROR) - returnStr += logStr + "<br />\n" - del sickbeard.loadingShowList[showDir] - - return returnStr - -class Flash: - _messages = [] - _errors = [] - - def message(self, title, detail=''): - Flash._messages.append((title, detail)) - - def error(self, title, detail=''): - Flash._errors.append((title, detail)) - - def messages(self): - tempMessages = Flash._messages - Flash._messages = [] - return tempMessages - - def errors(self): - tempErrors = Flash._errors - Flash._errors = [] - return tempErrors - -flash = Flash() diff --git a/sickbeard/versionChecker.py b/sickbeard/versionChecker.py index e91c7e0d0b9790764a0a0830e123ff2bb0cae65a..472d1b1b4981a9cd8ddde8a54c7a6089918d0ae2 100644 --- a/sickbeard/versionChecker.py +++ b/sickbeard/versionChecker.py @@ -17,11 +17,12 @@ # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. import sickbeard -from sickbeard import helpers, version, ui +from sickbeard import version, ui from sickbeard import logger from sickbeard import scene_exceptions +from sickbeard.exceptions import ex -import os, os.path, platform, shutil, time +import os, platform, shutil import subprocess, re import urllib, urllib2 import zipfile, tarfile @@ -89,7 +90,7 @@ class CheckVersion(): if not self.updater.need_update(): logger.log(u"No update needed") if force: - ui.flash.message('No update needed') + ui.notifications.message('No update needed') return False self.updater.set_newest_text() @@ -160,7 +161,7 @@ class WindowsUpdateManager(UpdateManager): # download the zip try: logger.log(u"Downloading update file from "+str(new_link)) - (filename, headers) = urllib.urlretrieve(new_link) + (filename, headers) = urllib.urlretrieve(new_link) #@UnusedVariable # prepare the update dir sb_update_dir = os.path.join(sickbeard.PROG_DIR, 'sb-update') @@ -177,7 +178,7 @@ class WindowsUpdateManager(UpdateManager): # find update dir name update_dir_contents = os.listdir(sb_update_dir) if len(update_dir_contents) != 1: - logger.log("Invalid update data, update failed.", logger.ERROR) + logger.log("Invalid update data, update failed. Maybe try deleting your sb-update folder?", logger.ERROR) return False content_dir = os.path.join(sb_update_dir, update_dir_contents[0]) @@ -191,7 +192,7 @@ class WindowsUpdateManager(UpdateManager): os.remove(filename) except Exception, e: - logger.log(u"Error while trying to update: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Error while trying to update: "+ex(e), logger.ERROR) return False return True @@ -233,7 +234,7 @@ class GitUpdateManager(UpdateManager): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, cwd=sickbeard.PROG_DIR) output, err = p.communicate() logger.log(u"git output: "+output, logger.DEBUG) - except OSError, e: + except OSError: logger.log(u"Command "+cmd+" didn't work, couldn't find git.") continue @@ -258,7 +259,7 @@ class GitUpdateManager(UpdateManager): Returns: True for success or False for failure """ - output, err = self._run_git('rev-parse HEAD') + output, err = self._run_git('rev-parse HEAD') #@UnusedVariable if not output: return self._git_error() @@ -328,7 +329,7 @@ class GitUpdateManager(UpdateManager): self._find_installed_version() try: self._check_github_for_update() - except Exception, e: + except Exception: logger.log(u"Unable to contact github, can't check for update", logger.ERROR) return False @@ -345,7 +346,7 @@ class GitUpdateManager(UpdateManager): on the call's success. """ - output, err = self._run_git('pull origin '+sickbeard.version.SICKBEARD_VERSION) + output, err = self._run_git('pull origin '+sickbeard.version.SICKBEARD_VERSION) #@UnusedVariable if not output: return self._git_error() @@ -462,7 +463,7 @@ class SourceUpdateManager(GitUpdateManager): content_dir = os.path.join(sb_update_dir, update_dir_contents[0]) # walk temp folder and move files to main folder - for dirname, dirnames, filenames in os.walk(content_dir): + for dirname, dirnames, filenames in os.walk(content_dir): #@UnusedVariable dirname = dirname[len(content_dir)+1:] for curfile in filenames: old_path = os.path.join(content_dir, dirname, curfile) @@ -478,7 +479,7 @@ class SourceUpdateManager(GitUpdateManager): ver_file.write(self._newest_commit_hash) ver_file.close() except IOError, e: - logger.log(u"Unable to write version file, update not complete: "+str(e).decode('utf-8'), logger.ERROR) + logger.log(u"Unable to write version file, update not complete: "+ex(e), logger.ERROR) return False return True diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 9d06b381d1493da5684bc2e391e780015b072d85..4b2ec79aa405fb6a3e34f6f64eab522385441e94 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -25,28 +25,25 @@ import urllib import re import threading import datetime -import operator from Cheetah.Template import Template -import cherrypy import cherrypy.lib -import metadata.helpers +import sickbeard from sickbeard import config -from sickbeard import history, notifiers, processTV, search, providers -from sickbeard import tv, versionChecker, ui +from sickbeard import history, notifiers, processTV +from sickbeard import tv, ui from sickbeard import logger, helpers, exceptions, classes, db from sickbeard import encodingKludge as ek from sickbeard import search_queue from sickbeard import image_cache -from sickbeard.notifiers import xbmc -from sickbeard.notifiers import plex from sickbeard.providers import newznab -from sickbeard.common import * +from sickbeard.common import Quality, Overview, statusStrings +from sickbeard.common import SNATCHED, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED +from sickbeard.exceptions import ex -from lib.tvdb_api import tvdb_exceptions from lib.tvdb_api import tvdb_api try: @@ -56,8 +53,6 @@ except ImportError: import xml.etree.cElementTree as etree -import sickbeard - from sickbeard import browser @@ -81,7 +76,6 @@ class PageTemplate (Template): { 'title': 'Config', 'key': 'config' }, { 'title': logPageTitle, 'key': 'errorlogs' }, ] - self.flash = ui.Flash() def redirect(abspath, *args, **KWs): assert abspath[0] == '/' @@ -139,9 +133,9 @@ class ManageSearches: def index(self): t = PageTemplate(file="manage_manageSearches.tmpl") #t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() - t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() - t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() - t.searchStatus = sickbeard.currentSearchScheduler.action.amActive + t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() #@UndefinedVariable + t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() #@UndefinedVariable + t.searchStatus = sickbeard.currentSearchScheduler.action.amActive #@UndefinedVariable t.submenu = ManageMenu return _munge(t) @@ -153,7 +147,7 @@ class ManageSearches: result = sickbeard.currentSearchScheduler.forceRun() if result: logger.log(u"Search forced") - ui.flash.message('Episode search started', + ui.notifications.message('Episode search started', 'Note: RSS feeds may not be updated if retrieved recently') redirect("/manage/manageSearches") @@ -161,9 +155,9 @@ class ManageSearches: @cherrypy.expose def pauseBacklog(self, paused=None): if paused == "1": - sickbeard.searchQueueScheduler.action.pause_backlog() + sickbeard.searchQueueScheduler.action.pause_backlog() #@UndefinedVariable else: - sickbeard.searchQueueScheduler.action.unpause_backlog() + sickbeard.searchQueueScheduler.action.unpause_backlog() #@UndefinedVariable redirect("/manage/manageSearches") @@ -171,7 +165,7 @@ class ManageSearches: def forceVersionCheck(self): # force a check to see if there is a new version - result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) + result = sickbeard.versionCheckScheduler.action.check_for_new_version(force=True) #@UndefinedVariable if result: logger.log(u"Forcing version check") @@ -284,7 +278,7 @@ class Manage: all_eps = [str(x["season"])+'x'+str(x["episode"]) for x in all_eps_results] to_change[cur_tvdb_id] = all_eps - result = Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) + Home().setStatus(cur_tvdb_id, '|'.join(to_change[cur_tvdb_id]), newStatus, direct=True) redirect('/manage/episodeStatuses') @@ -294,7 +288,7 @@ class Manage: show_obj = helpers.findCertainShow(sickbeard.showList, int(tvdb_id)) if show_obj: - sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) + sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) #@UndefinedVariable redirect("/manage/backlogOverview") @@ -450,7 +444,7 @@ class Manage: errors.append('<b>%s:</b><br />\n<ul>' % showObj.name + '\n'.join(['<li>%s</li>' % error for error in curErrors]) + "</ul>") if len(errors) > 0: - ui.flash.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), "<br />\n".join(errors)) redirect("/manage") @@ -505,25 +499,25 @@ class Manage: if curShowID in toUpdate: try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable updates.append(showObj.name) except exceptions.CantUpdateException, e: - errors.append("Unable to update show "+showObj.name+": "+str(e).decode('utf-8')) + errors.append("Unable to update show "+showObj.name+": "+ex(e)) # don't bother refreshing shows that were updated anyway if curShowID in toRefresh and curShowID not in toUpdate: try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable refreshes.append(showObj.name) except exceptions.CantRefreshException, e: - errors.append("Unable to refresh show "+showObj.name+": "+str(e).decode('utf-8')) + errors.append("Unable to refresh show "+showObj.name+": "+ex(e)) if curShowID in toRename: - sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) + sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) #@UndefinedVariable renames.append(showObj.name) if len(errors) > 0: - ui.flash.error("Errors encountered", + ui.notifications.error("Errors encountered", '<br >\n'.join(errors)) messageDetail = "" @@ -544,7 +538,7 @@ class Manage: messageDetail += "</li></ul>" if len(updates+refreshes+renames) > 0: - ui.flash.message("The following actions were queued:", + ui.notifications.message("The following actions were queued:", messageDetail) redirect("/manage") @@ -575,7 +569,7 @@ class History: myDB = db.DBConnection() myDB.action("DELETE FROM history WHERE 1=1") - ui.flash.message('History cleared') + ui.notifications.message('History cleared') redirect("/history") @@ -584,7 +578,7 @@ class History: myDB = db.DBConnection() myDB.action("DELETE FROM history WHERE date < "+str((datetime.datetime.today()-datetime.timedelta(days=30)).strftime(history.dateFormat))) - ui.flash.message('Removed history entries greater than 30 days old') + ui.notifications.message('Removed history entries greater than 30 days old') redirect("/history") @@ -680,10 +674,10 @@ class ConfigGeneral: if len(results) > 0: for x in results: logger.log(x, logger.ERROR) - ui.flash.error('Error(s) Saving Configuration', + ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) else: - ui.flash.message('Configuration Saved', ek.ek(os.path.join, sickbeard.PROG_DIR, 'config.ini') ) + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) redirect("/config/general/") @@ -761,10 +755,10 @@ class ConfigSearch: if len(results) > 0: for x in results: logger.log(x, logger.ERROR) - ui.flash.error('Error(s) Saving Configuration', + ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) else: - ui.flash.message('Configuration Saved', ek.ek(os.path.join, sickbeard.PROG_DIR, 'config.ini') ) + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) redirect("/config/search/") @@ -868,10 +862,10 @@ class ConfigPostProcessing: if len(results) > 0: for x in results: logger.log(x, logger.ERROR) - ui.flash.error('Error(s) Saving Configuration', + ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) else: - ui.flash.message('Configuration Saved', ek.ek(os.path.join, sickbeard.PROG_DIR, 'config.ini') ) + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) redirect("/config/postProcessing/") @@ -1024,12 +1018,12 @@ class ConfigProviders: @cherrypy.expose - def saveProviders(self, tvbinz_uid=None, tvbinz_hash=None, nzbs_org_uid=None, - nzbs_org_hash=None, nzbmatrix_username=None, nzbmatrix_apikey=None, - tvbinz_auth=None, provider_order=None, + def saveProviders(self, nzbs_org_uid=None, nzbs_org_hash=None, + nzbmatrix_username=None, nzbmatrix_apikey=None, nzbs_r_us_uid=None, nzbs_r_us_hash=None, newznab_string=None, tvtorrents_digest=None, tvtorrents_hash=None, - newzbin_username=None, newzbin_password=None): + newzbin_username=None, newzbin_password=None, + provider_order=None): results = [] @@ -1075,10 +1069,7 @@ class ConfigProviders: provider_list.append(curProvider) - if curProvider == 'tvbinz': - if curEnabled or sickbeard.SHOW_TVBINZ: - sickbeard.TVBINZ = curEnabled - elif curProvider == 'nzbs_org': + if curProvider == 'nzbs_org': sickbeard.NZBS = curEnabled elif curProvider == 'nzbs_r_us': sickbeard.NZBSRUS = curEnabled @@ -1099,13 +1090,6 @@ class ConfigProviders: else: logger.log(u"don't know what "+curProvider+" is, skipping") - if tvbinz_uid: - sickbeard.TVBINZ_UID = tvbinz_uid.strip() - if tvbinz_hash: - sickbeard.TVBINZ_HASH = tvbinz_hash.strip() - if tvbinz_auth: - sickbeard.TVBINZ_AUTH = tvbinz_auth.strip() - sickbeard.TVTORRENTS_DIGEST = tvtorrents_digest.strip() sickbeard.TVTORRENTS_HASH = tvtorrents_hash.strip() @@ -1128,10 +1112,10 @@ class ConfigProviders: if len(results) > 0: for x in results: logger.log(x, logger.ERROR) - ui.flash.error('Error(s) Saving Configuration', + ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) else: - ui.flash.message('Configuration Saved', ek.ek(os.path.join, sickbeard.PROG_DIR, 'config.ini') ) + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) redirect("/config/providers/") @@ -1318,10 +1302,10 @@ class ConfigNotifications: if len(results) > 0: for x in results: logger.log(x, logger.ERROR) - ui.flash.error('Error(s) Saving Configuration', + ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) else: - ui.flash.message('Configuration Saved', ek.ek(os.path.join, sickbeard.PROG_DIR, 'config.ini') ) + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE) ) redirect("/config/notifications/") @@ -1426,7 +1410,7 @@ class NewHomeAddShows: try: seriesXML = etree.ElementTree(etree.XML(urlData)) except Exception, e: - logger.log(u"Unable to parse XML for some reason: "+str(e).decode('utf-8')+" from XML: "+urlData, logger.ERROR) + logger.log(u"Unable to parse XML for some reason: "+ex(e)+" from XML: "+urlData, logger.ERROR) return '' series = seriesXML.getiterator('Series') @@ -1601,14 +1585,14 @@ class NewHomeAddShows: # blanket policy - if the dir exists you should have used "add existing show" numbnuts if ek.ek(os.path.isdir, show_dir) and not fullShowPath: - ui.flash.error("Unable to add show", "Folder "+show_dir+" exists already") + ui.notifications.error("Unable to add show", "Folder "+show_dir+" exists already") redirect('/home') # create the dir and make sure it worked dir_exists = helpers.makeDir(show_dir) if not dir_exists: logger.log(u"Unable to create the folder "+show_dir+", can't add the show", logger.ERROR) - ui.flash.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") + ui.notifications.error("Unable to add show", "Unable to create the folder "+show_dir+", can't add the show") redirect("/home") else: helpers.chmodAsParent(show_dir) @@ -1630,8 +1614,8 @@ class NewHomeAddShows: newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, seasonFolders, tvdbLang) - ui.flash.message('Show added', 'Adding the specified show into '+show_dir) + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, int(defaultStatus), newQuality, seasonFolders, tvdbLang) #@UndefinedVariable + ui.notifications.message('Show added', 'Adding the specified show into '+show_dir) return finishAddShow() @@ -1701,11 +1685,11 @@ class NewHomeAddShows: show_dir, tvdb_id, show_name = cur_show # add the show - sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.SEASON_FOLDERS_DEFAULT) + sickbeard.showQueueScheduler.action.addShow(tvdb_id, show_dir, SKIPPED, sickbeard.QUALITY_DEFAULT, sickbeard.SEASON_FOLDERS_DEFAULT) #@UndefinedVariable num_added += 1 if num_added: - ui.flash.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") + ui.notifications.message("Shows Added", "Automatically added "+str(num_added)+" from their existing metadata files") # if we're done then go home if not dirs_only: @@ -1748,8 +1732,8 @@ class ErrorLogs: minLevel = int(minLevel) data = [] - if os.path.isfile(logger.logFile): - f = ek.ek(open, logger.logFile) + if os.path.isfile(logger.sb_log_instance.log_file): + f = ek.ek(open, logger.sb_log_instance.log_file) data = f.readlines() f.close() @@ -1828,9 +1812,9 @@ class Home: pw_append = " with password: " + password if result: - return "Test growl sent successfully to "+urllib.unquote_plus(host)+pw_append + return "Registered and Tested growl successfully "+urllib.unquote_plus(host)+pw_append else: - return "Test growl failed to "+urllib.unquote_plus(host)+pw_append + return "Registration and Testing of growl failed "+urllib.unquote_plus(host)+pw_append @cherrypy.expose def testProwl(self, prowl_api=None, prowl_priority=0): @@ -1959,7 +1943,7 @@ class Home: if str(pid) != str(sickbeard.PID): redirect("/home") - updated = sickbeard.versionCheckScheduler.action.update() + updated = sickbeard.versionCheckScheduler.action.update() #@UndefinedVariable if updated: # do a hard restart @@ -2001,23 +1985,25 @@ class Home: except sickbeard.exceptions.ShowDirNotFoundException: t.showLoc = (showObj._location, False) - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): - ui.flash.message('This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.') + show_message = '' - elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): - ui.flash.message('The information below is in the process of being updated.') + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + show_message = 'This show is in the process of being downloaded from theTVDB.com - the info below is incomplete.' - elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): - ui.flash.message('The episodes below are currently being refreshed from disk') + elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable + show_message = 'The information below is in the process of being updated.' - elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): - ui.flash.message('This show is queued to be refreshed.') + elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): #@UndefinedVariable + show_message = 'The episodes below are currently being refreshed from disk' - elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): - ui.flash.message('This show is queued and awaiting an update.') + elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued to be refreshed.' - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): + elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): #@UndefinedVariable + show_message = 'This show is queued and awaiting an update.' + + if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable + if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable t.submenu.append({ 'title': 'Delete', 'path': 'home/deleteShow?show=%d'%showObj.tvdbid, 'confirm': True }) t.submenu.append({ 'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d'%showObj.tvdbid }) t.submenu.append({ 'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1'%showObj.tvdbid }) @@ -2027,6 +2013,7 @@ class Home: t.show = showObj t.sqlResults = sqlResults t.seasonResults = seasonResults + t.show_message = show_message epCounts = {} epCats = {} @@ -2130,9 +2117,9 @@ class Home: if bool(showObj.seasonfolders) != bool(seasonfolders): showObj.seasonfolders = seasonfolders try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show: "+str(e).decode('utf-8')) + errors.append("Unable to refresh this show: "+ex(e)) showObj.paused = paused showObj.air_by_date = air_by_date @@ -2150,9 +2137,9 @@ class Home: try: showObj.location = location try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show:"+str(e).decode('utf-8')) + errors.append("Unable to refresh this show:"+ex(e)) # grab updated info from TVDB #showObj.loadEpisodesFromTVDB() # rescan the episodes in the new folder @@ -2165,7 +2152,7 @@ class Home: # force the update if do_update: try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) + sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable time.sleep(1) except exceptions.CantUpdateException, e: errors.append("Unable to force an update on the show.") @@ -2174,7 +2161,7 @@ class Home: return errors if len(errors) > 0: - ui.flash.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), + ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), '<ul>' + '\n'.join(['<li>%s</li>' % error for error in errors]) + "</ul>") redirect("/home/displayShow?show=" + show) @@ -2190,13 +2177,12 @@ class Home: if showObj == None: return _genericMessage("Error", "Unable to find the specified show") - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or \ - sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable return _genericMessage("Error", "Shows can't be deleted while they're being added or updated.") showObj.deleteShow() - ui.flash.message('<b>%s</b> has been deleted' % showObj.name) + ui.notifications.message('<b>%s</b> has been deleted' % showObj.name) redirect("/home") @cherrypy.expose @@ -2212,10 +2198,10 @@ class Home: # force the update from the DB try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) + sickbeard.showQueueScheduler.action.refreshShow(showObj) #@UndefinedVariable except exceptions.CantRefreshException, e: - ui.flash.error("Unable to refresh this show.", - str(e)) + ui.notifications.error("Unable to refresh this show.", + ex(e)) time.sleep(3) @@ -2234,10 +2220,10 @@ class Home: # force the update try: - sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) + sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) #@UndefinedVariable except exceptions.CantUpdateException, e: - ui.flash.error("Unable to update this show.", - str(e)) + ui.notifications.error("Unable to update this show.", + ex(e)) # just give it some time time.sleep(3) @@ -2250,9 +2236,9 @@ class Home: for curHost in [x.strip() for x in sickbeard.XBMC_HOST.split(",")]: if notifiers.xbmc_notifier._update_library(curHost, showName=showName): - ui.flash.message("Command sent to XBMC host " + curHost + " to update library") + ui.notifications.message("Command sent to XBMC host " + curHost + " to update library") else: - ui.flash.error("Unable to contact XBMC host " + curHost) + ui.notifications.error("Unable to contact XBMC host " + curHost) redirect('/home') @@ -2260,10 +2246,10 @@ class Home: def updatePLEX(self): if notifiers.plex_notifier._update_library(): - ui.flash.message("Command sent to Plex Media Server host " + sickbeard.PLEX_HOST + " to update library") + ui.notifications.message("Command sent to Plex Media Server host " + sickbeard.PLEX_HOST + " to update library") logger.log(u"Plex library update initiated for host " + sickbeard.PLEX_HOST, logger.DEBUG) else: - ui.flash.error("Unable to contact Plex Media Server host " + sickbeard.PLEX_HOST) + ui.notifications.error("Unable to contact Plex Media Server host " + sickbeard.PLEX_HOST) logger.log(u"Plex library update failed for host " + sickbeard.PLEX_HOST, logger.ERROR) redirect('/home') @@ -2279,7 +2265,7 @@ class Home: if showObj == None: return _genericMessage("Error", "Unable to find the specified show") - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): #@UndefinedVariable return _genericMessage("Error", "Show is still being added, wait until it is finished before you rename files") showObj.fixEpisodeNames() @@ -2292,7 +2278,7 @@ class Home: if show == None or eps == None or status == None: errMsg = "You must specify a show and at least one episode" if direct: - ui.flash.error('Error', errMsg) + ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: return _genericMessage("Error", errMsg) @@ -2300,7 +2286,7 @@ class Home: if not statusStrings.has_key(int(status)): errMsg = "Invalid status" if direct: - ui.flash.error('Error', errMsg) + ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: return _genericMessage("Error", errMsg) @@ -2310,7 +2296,7 @@ class Home: if showObj == None: errMsg = "Error", "Show not in show list" if direct: - ui.flash.error('Error', errMsg) + ui.notifications.error('Error', errMsg) return json.dumps({'result': 'error'}) else: return _genericMessage("Error", errMsg) @@ -2358,11 +2344,11 @@ class Home: msg += "<li>Season "+str(cur_segment)+"</li>" logger.log(u"Sending backlog for "+showObj.name+" season "+str(cur_segment)+" because some eps were set to wanted") cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, cur_segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable msg += "</ul>" if segment_list: - ui.flash.message("Backlog started", msg) + ui.notifications.message("Backlog started", msg) if direct: return json.dumps({'result': 'success'}) @@ -2372,40 +2358,46 @@ class Home: @cherrypy.expose def searchEpisode(self, show=None, season=None, episode=None): - outStr = "" - epObj = _getEpisode(show, season, episode) - - if isinstance(epObj, str): - return _genericMessage("Error", epObj) + # retrieve the episode object and fail if we can't get one + ep_obj = _getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) - tempStr = "Searching for download for " + epObj.prettyName(True) - logger.log(tempStr) - outStr += tempStr + "<br />\n" - foundEpisode = search.findEpisode(epObj, manualSearch=True) + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj) + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) #@UndefinedVariable - if not foundEpisode: - message = 'No downloads were found' - ui.flash.error(message, "Couldn't find a download for <i>%s</i>" % epObj.prettyName(True)) - logger.log(message) + # wait until the queue item tells us whether it worked or not + while ep_queue_item.success == None: #@UndefinedVariable + time.sleep(1) - else: + # return the correct json value + if ep_queue_item.success: + return json.dumps({'result': statusStrings[ep_obj.status]}) - # just use the first result for now - logger.log(u"Downloading episode from " + foundEpisode.url) - result = search.snatchEpisode(foundEpisode) - providerModule = foundEpisode.provider - if not result: - ui.flash.error('Error while attempting to snatch '+foundEpisode.name+', check your logs') - elif providerModule == None: - ui.flash.error('Provider is configured incorrectly, unable to download') - else: - ui.flash.message('Episode <b>%s</b> snatched from <b>%s</b>' % (foundEpisode.name, providerModule.name)) + return json.dumps({'result': 'failure'}) - #TODO: check if the download was successful - - redirect("/home/displayShow?show=" + str(epObj.show.tvdbid)) +class UI: + + @cherrypy.expose + def add_message(self): + + ui.notifications.message('Test 1', 'This is test number 1') + ui.notifications.error('Test 2', 'This is test number 2') + + return "ok" + @cherrypy.expose + def get_messages(self): + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(): + messages['notification-'+str(cur_notification_num)] = {'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type} + cur_notification_num += 1 + return json.dumps(messages) class WebInterface: @@ -2560,3 +2552,5 @@ class WebInterface: browser = browser.WebFileBrowser() errorlogs = ErrorLogs() + + ui = UI() diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 26275c159f733ee52bcafdd98633122454ef3ffc..42fac12fff71f1576ae64f19bb343bd6a5802903 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with Sick Beard. If not, see <http://www.gnu.org/licenses/>. -import cherrypy +#import cherrypy import cherrypy.lib.auth_basic import os.path diff --git a/tests/scene_helpers_tests.py b/tests/scene_helpers_tests.py index 6f4fc626b2c5a5277de4738ee735c3e8716b4c80..36206d646bd68f05d52b9ab7c78216c3b0af2468 100644 --- a/tests/scene_helpers_tests.py +++ b/tests/scene_helpers_tests.py @@ -3,7 +3,7 @@ import unittest import sys, os.path sys.path.append(os.path.abspath('..')) -from sickbeard import show_name_helpers, scene_exceptions, common +from sickbeard import show_name_helpers, scene_exceptions, common, name_cache import sickbeard from sickbeard import db @@ -79,6 +79,7 @@ class SceneTests(unittest.TestCase): def test_filterBadReleases(self): self._test_filterBadReleases('Show.S02.German.Stuff-Grp', False) + self._test_filterBadReleases('Show.S02.Some.Stuff-Core2HD', False) self._test_filterBadReleases('Show.S02.Some.German.Stuff-Grp', False) self._test_filterBadReleases('German.Show.S02.Some.Stuff-Grp', True) self._test_filterBadReleases('Show.S02.This.Is.German', False) @@ -107,6 +108,24 @@ class SceneExceptionTestCase(unittest.TestCase): def test_sceneExceptionByNameEmpty(self): self.assertEqual(scene_exceptions.get_scene_exception_by_name('nothing useful'), None) + def test_sceneExceptionsResetNameCache(self): + # clear the exceptions + myDB = db.DBConnection("cache.db") + myDB.action("DELETE FROM scene_exceptions") + + # put something in the cache + name_cache.addNameToCache('Cached Name', 0) + + # updating should clear the cache so our previously "Cached Name" won't be in there + scene_exceptions.retrieve_exceptions() + self.assertEqual(name_cache.retrieveNameFromCache('Cached Name'), None) + + # put something in the cache + name_cache.addNameToCache('Cached Name', 0) + + # updating should not clear the cache this time since our exceptions didn't change + scene_exceptions.retrieve_exceptions() + self.assertEqual(name_cache.retrieveNameFromCache('Cached Name'), 0) if __name__ == '__main__':