Merge branch 'release/0.8.0'

This commit is contained in:
Adam 2015-04-13 22:04:25 +08:00
commit 3e8051c0b0
113 changed files with 12428 additions and 12226 deletions

View file

@ -1,3 +1,104 @@
### 0.8.0 (2015-04-13 14:00:00 UTC)
* Change Wombles to use tv-dvd section
* Add requirements file for pip (port from midgetspy/sick-beard)
* Remove unused libraries fuzzywuzzy and pysrt
* Change webserve code to a logical layout and PEP8
* Add text to explain params passed to extra scripts on Config/Post Processing
* Remove unused SickBeardURLOpener and AuthURLOpener classes
* Update Pushbullet notifier (port from midgetspy/sickbeard)
* Change startup code cleanup and PEP8
* Change authentication credentials to display more securely on config pages
* Add a "Use as default home page" selector to General Config/Interface/User Interface
* Add option to the third step of "Add Show" to set episodes as wanted from the first and latest season, this triggers
a backlog search on those episodes after the show is added
* Change to improve the integrity of the already post processed video checker
* Add Kodi notifier and metadata
* Add priority, device, and sound support to Pushover notifier (port from midgetspy/sickbeard)
* Fix updating of pull requests
* Add hidden cache debug page
* Change autoProcessTV scripts python code quotes from " -> '
* Add expand all button to Episode Status Management
* Add Unknown status query to Episode Status Management
* Fix Episode Status Management error popup from coming up when show is selected without expanding
* Add BET network logo
* Change "Force Backlog" button for paused shows on Backlog Overview page to "Paused" indicator
* Remove unused force variable from code and PEP8
* Change browser, bs4 parser and classes code to PEP8 standards
* Change common and config code to PEP8 standards
* Change database code to PEP8 standards
* Change general config's branches and pull request list generation for faster page loading
* Add PlayStation Network logo
* Change layout of Recent Search code
* Change naming of SEARCHQUEUE threads for shorter log lines
* Fix Recent Search running status on Manage Searches page
* Change to no longer require restart with the "Scan and post process" option on page config/Post Processing
* Add validation when using Release Group token on page config Post Processing/Episode Naming/Name pattern/Custom
* Change to simplify and reduce logging output of Recent-Search and Backlog processes
* Hide year, runtime, genre tags, country flag, or status if lacking valid data to display
* Remove redundant CSS color use (all browsers treat 3 identical digits as 6, except for possibly in gradients)
* Remove whitespace and semi-colon redundancy from CSS shedding 4.5kb
* Add show names to items listed during startup in the loading from database phase
* Add "Enable IMDb info" option to config/General/Interface
* Change to not display IMDb info on UI when "Enable IMDb info" is disabled
* Change genre tags on displayShow page to link to IMDb instead of Trakt
* Change to reduce the time taken to "Update shows" with show data
* Change to stop updating the IMDb info on edit, and during the scheduled daily update for every show
* Change to update the IMDb info for a show after snatching an episode for it
* Add IMDb lookup to "Update" action on Manage/Mass Update page
* Fix updating of scene exception name cache after adding exceptions on Editshow page
* Change log rotation to occur at midnight
* Change to keep a maximum of 7 log files
* Add automatic compression of old log files
* Change overhaul menu and button icons
* Add "Status of removed episodes" to apply (or not) a preferred status to episodes whose files are detected as removed.
"Archived" can now be set so that removed episodes still count toward download completion stats. See setting on page
config/Post Processing/File Handling
* Remove redundant "Skip remove detection" from the config/Post Processing/File Handling page
* Change to highlight the current selected item in combos on page config/Post Processing
* Change the episodes downloaded stat to display e.g. 2843 / 2844 as 99.9% instead of rounding to 100%
* Change 'never' episode row color away from blue on Display Show page when indexer airdate is not defined
* Add tint to archived episode row colour to differentiate it from downloaded episodes on the Display Show page
* Add indication of shows with never aired episodes on Episode Overview page
* Add "Collapse" button and visuals for Expanding... and Collapsing... states
* Add the number of episodes marked with the status being queried to Episode Overview page
* Add indication of shows with never aired episodes on Episode Overview page
* Change to separate "Set as wanted" to prevent disaster selection on Episode Overview page
* Remove restriction to not display snatched eps link in footer on Episode Overview page
* Change the shows episodes count text colour to visually separete from year numbers at the end of show names
* Change to add clarity to the subtitle and other columns on the Mass Update page
* Change to improve clarity with "Recent search" and "Limited backlog" on the Config/Search Settings page
* Change vertical alignment of input fields to be inline with text
* Add tooltips to explain why any the 6 action columns are disabled when required on the Mass Update page
* Change to reclaimed screen estate by hiding unused columns on the Mass Update page
* Change order of option on Mass Edit page to be inline with show edit page
* Fix release group not recognised from manually downloaded filename
* Change to gracefully handle some "key not found" failures when TVDB or TVRage return "Not Found" during show updates
* Change no longer stamp files where airdates are never
* Change overhaul displayShow to ready for new features
* Add section for show plot to displayShow
* Add option to view show background on displayShow (transparent and opaque) for when background downloading is added (disabled)
* Add option to collapse seasons and leave current season open on displayShow (disabled)
* Add filesize to episode location qtip on displayShow
* Change selected options from editShow will only show when enabled now on displayShow
* Change some label tags to fit with edit show page on displayShow
* Fix handle when a show in db has all episodes removed from indexer on displayShow
* Add the name of show that will be displayed to the hover of the Prev/Next show buttons on displayShow
* Add hover tooltips for nfo and tbn columns for browsers that use the title attr on displayShow
* Change Special link moved from "Season" line to "Specials" line on displayShow
* Change code re-factored in readiness for live option switching, clean up and add closures of html tables
* Add show overview from indexers to the database
* Fix case where start year or runtime is not available to display show
* Add "File logging level" to General Config/Advanced Settings
* Fix saving of Sort By/Next Episode in Layout Poster on Show List page
* Change improve backlog search
* Change only add valid items to save to DB
* Change provider cache storage structure
* Add handling for failed cache database upgrades
* Fix XEM Exceptions in case of bad data from XEM
* Change order of snatched provider images to chronological on History layout compact and add ordinal indicators in the tooltips
### 0.7.2 (2015-03-10 17:05:00 UTC)
* Fix Add From Trending page (indexer_id can be "None" which causes white screen when clicking "Add Show")
@ -86,7 +187,7 @@
* Change SCC URLs to remove redirection overhead
* Change TorrentBytes login parameter in line with site change
* Change FreshOnTv login parameter and use secure URLs, add logging of Cloudflare blocking and prevent vacant cookie tracebacks
* Change TPB webproxy list and add SSL variants
* Change TPB webproxy list and add SSL variants
* Add YTV network logo
* Remove defunct Fanzub provider

View file

@ -25,9 +25,14 @@ import signal
import sys
import shutil
import subprocess
import os
import locale
import datetime
import threading
import getopt
if sys.version_info < (2, 6):
print "Sorry, requires Python 2.6 or 2.7."
print 'Sorry, requires Python 2.6 or 2.7.'
sys.exit(1)
try:
@ -36,27 +41,20 @@ try:
if Cheetah.Version[0] != '2':
raise ValueError
except ValueError:
print "Sorry, requires Python module Cheetah 2.1.0 or newer."
print 'Sorry, requires Python module Cheetah 2.1.0 or newer.'
sys.exit(1)
except:
print "The Python module Cheetah is required"
print 'The Python module Cheetah is required'
sys.exit(1)
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib')))
# 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 # @UnresolvedImport
import locale
import datetime
import threading
import getopt
import sickbeard
from sickbeard import db, logger, network_timezones, failed_history, name_cache, versionChecker
from sickbeard import db, logger, network_timezones, failed_history, name_cache
from sickbeard.tv import TVShow
from sickbeard.webserveInit import WebServer
from sickbeard.databases.mainDB import MIN_DB_VERSION, MAX_DB_VERSION
@ -85,35 +83,36 @@ class SickGear(object):
self.forcedPort = None
self.noLaunch = False
def help_message(self):
@staticmethod
def help_message():
"""
print help message for commandline options
"""
help_msg = "\n"
help_msg += "Usage: " + sickbeard.MY_FULLNAME + " <option> <another option>\n"
help_msg += "\n"
help_msg += "Options:\n"
help_msg += "\n"
help_msg += " -h --help Prints this message\n"
help_msg += " -f --forceupdate Force update all shows in the DB (from tvdb) on startup\n"
help_msg += " -q --quiet Disables logging to console\n"
help_msg += " --nolaunch Suppress launching web browser on startup\n"
help_msg = '\n'
help_msg += 'Usage: %s <option> <another option>\n' % sickbeard.MY_FULLNAME
help_msg += '\n'
help_msg += 'Options:\n'
help_msg += '\n'
help_msg += ' -h --help Prints this message\n'
help_msg += ' -f --forceupdate Force update all shows in the DB (from tvdb) on startup\n'
help_msg += ' -q --quiet Disables logging to console\n'
help_msg += ' --nolaunch Suppress launching web browser on startup\n'
if sys.platform == 'win32':
help_msg += " -d --daemon Running as real daemon is not supported on Windows\n"
help_msg += " On Windows, --daemon is substituted with: --quiet --nolaunch\n"
help_msg += ' -d --daemon Running as real daemon is not supported on Windows\n'
help_msg += ' On Windows, --daemon is substituted with: --quiet --nolaunch\n'
else:
help_msg += " -d --daemon Run as double forked daemon (includes options --quiet --nolaunch)\n"
help_msg += " --pidfile=<path> Combined with --daemon creates a pidfile (full path including filename)\n"
help_msg += ' -d --daemon Run as double forked daemon (includes options --quiet --nolaunch)\n'
help_msg += ' --pidfile=<path> Combined with --daemon creates a pidfile (full path including filename)\n'
help_msg += " -p <port> --port=<port> Override default/configured port to listen on\n"
help_msg += " --datadir=<path> Override folder (full path) as location for\n"
help_msg += " storing database, configfile, cache, logfiles \n"
help_msg += " Default: " + sickbeard.PROG_DIR + "\n"
help_msg += " --config=<path> Override config filename (full path including filename)\n"
help_msg += " to load configuration from \n"
help_msg += " Default: config.ini in " + sickbeard.PROG_DIR + " or --datadir location\n"
help_msg += " --noresize Prevent resizing of the banner/posters even if PIL is installed\n"
help_msg += ' -p <port> --port=<port> Override default/configured port to listen on\n'
help_msg += ' --datadir=<path> Override folder (full path) as location for\n'
help_msg += ' storing database, configfile, cache, logfiles \n'
help_msg += ' Default: %s\n' % sickbeard.PROG_DIR
help_msg += ' --config=<path> Override config filename (full path including filename)\n'
help_msg += ' to load configuration from \n'
help_msg += ' Default: config.ini in %s or --datadir location\n' % sickbeard.PROG_DIR
help_msg += ' --noresize Prevent resizing of the banner/posters even if PIL is installed\n'
return help_msg
@ -127,7 +126,7 @@ class SickGear(object):
sickbeard.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
locale.setlocale(locale.LC_ALL, '')
except (locale.Error, IOError):
pass
try:
@ -139,7 +138,7 @@ class SickGear(object):
if not sickbeard.SYS_ENCODING or sickbeard.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
sickbeard.SYS_ENCODING = 'UTF-8'
if not hasattr(sys, "setdefaultencoding"):
if not hasattr(sys, 'setdefaultencoding'):
reload(sys)
try:
@ -148,17 +147,17 @@ class SickGear(object):
sys.setdefaultencoding(sickbeard.SYS_ENCODING)
except:
print 'Sorry, you MUST add the SickGear folder to the PYTHONPATH environment variable'
print 'or find another way to force Python to use ' + sickbeard.SYS_ENCODING + ' for string encoding.'
print 'or find another way to force Python to use %s for string encoding.' % sickbeard.SYS_ENCODING
sys.exit(1)
# Need console logging for SickBeard.py and SickBeard-console.exe
self.consoleLogging = (not hasattr(sys, "frozen")) or (sickbeard.MY_NAME.lower().find('-console') > 0)
self.consoleLogging = (not hasattr(sys, 'frozen')) or (sickbeard.MY_NAME.lower().find('-console') > 0)
# Rename the main thread
threading.currentThread().name = "MAIN"
threading.currentThread().name = 'MAIN'
try:
opts, args = getopt.getopt(sys.argv[1:], "hfqdp::",
opts, args = getopt.getopt(sys.argv[1:], 'hfqdp::',
['help', 'forceupdate', 'quiet', 'nolaunch', 'daemon', 'pidfile=', 'port=',
'datadir=', 'config=', 'noresize']) # @UnusedVariable
except getopt.GetoptError:
@ -188,7 +187,7 @@ class SickGear(object):
try:
self.forcedPort = int(a)
except ValueError:
sys.exit("Port: " + str(a) + " is not a number. Exiting.")
sys.exit('Port: %s is not a number. Exiting.' % a)
# Run as a double forked daemon
if o in ('-d', '--daemon'):
@ -207,7 +206,7 @@ class SickGear(object):
# If the pidfile already exists, sickbeard may still be running, so exit
if os.path.exists(self.PIDFILE):
sys.exit("PID file: " + self.PIDFILE + " already exists. Exiting.")
sys.exit('PID file: %s already exists. Exiting.' % self.PIDFILE)
# Specify folder to load the config file from
if o in ('--config',):
@ -238,14 +237,14 @@ class SickGear(object):
# 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")
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:
sys.exit(u'Unable to create data directory: %s + Exiting.' % sickbeard.DATA_DIR)
sys.exit(u'Unable to create data directory: %s Exiting.' % sickbeard.DATA_DIR)
# Make sure we can write to the data dir
if not os.access(sickbeard.DATA_DIR, os.W_OK):
@ -261,7 +260,7 @@ class SickGear(object):
os.chdir(sickbeard.DATA_DIR)
if self.consoleLogging:
print u'Starting up SickGear from %s' % sickbeard.CONFIG_FILE
print u'Starting up SickGear from %s' % sickbeard.CONFIG_FILE
# Load the config and publish it to the sickbeard package
if not os.path.isfile(sickbeard.CONFIG_FILE):
@ -279,7 +278,8 @@ class SickGear(object):
if CUR_DB_VERSION > MAX_DB_VERSION:
print u'Your database version (%s) has been incremented past what this version of SickGear supports' \
% CUR_DB_VERSION
sys.exit(u'If you have used other forks of SB, your database may be unusable due to their modifications')
sys.exit(
u'If you have used other forks of SG, your database may be unusable due to their modifications')
# Initialize the config and our threads
sickbeard.initialize(consoleLogging=self.consoleLogging)
@ -291,7 +291,7 @@ class SickGear(object):
sickbeard.PID = os.getpid()
if self.forcedPort:
logger.log(u"Forcing web server to port " + str(self.forcedPort))
logger.log(u'Forcing web server to port %s' % self.forcedPort)
self.startPort = self.forcedPort
else:
self.startPort = sickbeard.WEB_PORT
@ -334,10 +334,10 @@ class SickGear(object):
self.webserver = WebServer(self.web_options)
self.webserver.start()
except Exception:
logger.log(u"Unable to start web server, is something else running on port %d?" % self.startPort,
logger.log(u'Unable to start web server, is something else running on port %d?' % self.startPort,
logger.ERROR)
if sickbeard.LAUNCH_BROWSER and not self.runAsDaemon:
logger.log(u"Launching browser and exiting", logger.ERROR)
logger.log(u'Launching browser and exiting', logger.ERROR)
sickbeard.launchBrowser(self.startPort)
os._exit(1)
@ -345,9 +345,9 @@ class SickGear(object):
restoreDir = os.path.join(sickbeard.DATA_DIR, 'restore')
if os.path.exists(restoreDir):
if self.restore(restoreDir, sickbeard.DATA_DIR):
logger.log(u"Restore successful...")
logger.log(u'Restore successful...')
else:
logger.log_error_and_exit(u"Restore FAILED!", logger.ERROR)
logger.log_error_and_exit(u'Restore FAILED!')
# Build from the DB to start with
self.loadShowsFromDB()
@ -374,7 +374,7 @@ class SickGear(object):
sickbeard.launchBrowser(self.startPort)
# main loop
while (True):
while True:
time.sleep(1)
def daemonize(self):
@ -388,7 +388,7 @@ class SickGear(object):
if pid != 0:
os._exit(0)
except OSError, e:
sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.stderr.write('fork #1 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
os.setsid() # @UndefinedVariable - only available in UNIX
@ -403,19 +403,18 @@ class SickGear(object):
if pid != 0:
os._exit(0)
except OSError, e:
sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
sys.stderr.write('fork #2 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)
# Write pid
if self.CREATEPID:
pid = str(os.getpid())
logger.log(u"Writing PID: " + pid + " to " + str(self.PIDFILE))
logger.log(u'Writing PID: %s to %s' % (pid, self.PIDFILE))
try:
file(self.PIDFILE, 'w').write("%s\n" % pid)
file(self.PIDFILE, 'w').write('%s\n' % pid)
except IOError, e:
logger.log_error_and_exit(
u"Unable to write PID file: " + self.PIDFILE + " Error: " + str(e.strerror) + " [" + str(
e.errno) + "]")
u'Unable to write PID file: %s Error: %s [%s]' % (self.PIDFILE, e.strerror, e.errno))
# Redirect all output
sys.stdout.flush()
@ -429,7 +428,8 @@ class SickGear(object):
os.dup2(stdout.fileno(), sys.stdout.fileno())
os.dup2(stderr.fileno(), sys.stderr.fileno())
def remove_pid_file(self, PIDFILE):
@staticmethod
def remove_pid_file(PIDFILE):
try:
if os.path.exists(PIDFILE):
os.remove(PIDFILE)
@ -439,26 +439,27 @@ class SickGear(object):
return True
def loadShowsFromDB(self):
@staticmethod
def loadShowsFromDB():
"""
Populates the showList with shows from the database
"""
logger.log(u"Loading initial show list")
logger.log(u'Loading initial show list')
myDB = db.DBConnection()
sqlResults = myDB.select("SELECT * FROM tv_shows")
sqlResults = myDB.select('SELECT * FROM tv_shows')
sickbeard.showList = []
for sqlShow in sqlResults:
try:
curShow = TVShow(int(sqlShow["indexer"]), int(sqlShow["indexer_id"]))
curShow = TVShow(int(sqlShow['indexer']), int(sqlShow['indexer_id']))
curShow.nextEpisode()
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',
'replace'),
u'There was an error creating the show in %s: %s' % (sqlShow['location'], str(e).decode('utf-8',
'replace')),
logger.ERROR)
def restore(self, srcDir, dstDir):
@ -485,7 +486,7 @@ class SickGear(object):
# shutdown web server
if self.webserver:
logger.log("Shutting down Tornado")
logger.log('Shutting down Tornado')
self.webserver.shutDown()
try:
self.webserver.join(10)
@ -508,7 +509,7 @@ class SickGear(object):
popen_list += sickbeard.MY_ARGS
if '--nolaunch' not in popen_list:
popen_list += ['--nolaunch']
logger.log(u"Restarting SickGear with " + str(popen_list))
logger.log(u'Restarting SickGear with %s' % popen_list)
logger.close()
subprocess.Popen(popen_list, cwd=os.getcwd())
@ -516,7 +517,7 @@ class SickGear(object):
os._exit(0)
if __name__ == "__main__":
if __name__ == '__main__':
if sys.hexversion >= 0x020600F0:
freeze_support()

View file

@ -24,7 +24,7 @@ import sys
import autoProcessTV
if len(sys.argv) < 4:
print "No folder supplied - is this being called from HellaVCR?"
print 'No folder supplied - is this being called from HellaVCR?'
sys.exit()
else:
autoProcessTV.processEpisode(sys.argv[3], sys.argv[2])

View file

@ -23,7 +23,7 @@ import sys
import autoProcessTV
if len(sys.argv) < 2:
print "No folder supplied - is this being called from SABnzbd?"
print 'No folder supplied - is this being called from SABnzbd?'
sys.exit()
elif len(sys.argv) >= 8:
autoProcessTV.processEpisode(sys.argv[1], sys.argv[2], sys.argv[7])

View file

@ -1,50 +1,58 @@
#fileBrowserDialog {
max-height: 480px;
overflow-y: auto;
}
#fileBrowserDialog ul {
margin: 0;
padding: 0;
}
#fileBrowserDialog ul li {
margin: 2px 0;
cursor: pointer;
list-style-type: none;
}
#fileBrowserDialog ul li a {
display: block;
padding: 4px 0;
}
#fileBrowserDialog ul li a:hover {
color: #09A2FF;
background: none;
}
#fileBrowserDialog ul li a span.ui-icon {
margin: 0 4px;
float: left;
#fileBrowserDialog{
max-height:480px;
overflow-y:auto
}
#fileBrowserDialog ul{
margin:0;
padding:0
}
#fileBrowserDialog ul li{
margin:2px 0;
cursor:pointer;
list-style-type:none
}
#fileBrowserDialog ul li a{
display:block;
padding:4px 0
}
#fileBrowserDialog ul li a:hover{
color:#09A2FF;
background:none
}
#fileBrowserDialog ul li a span.ui-icon{
margin:0 4px;
float:left
}
/* jquery ui autocomplete overrides to make it look more like the old autocomplete */
.ui-autocomplete {
max-height: 180px;
overflow-y: auto;
/* prevent horizontal scrollbar */
overflow-x: hidden;
/* add padding to account for vertical scrollbar */
padding-right: 20px;
.ui-autocomplete{
max-height:180px;
overflow-y:auto;
/* prevent horizontal scrollbar */
overflow-x:hidden;
/* add padding to account for vertical scrollbar */
padding-right:20px
}
* html .ui-autocomplete {
height: 180px;
* html .ui-autocomplete{
height:180px
}
.ui-menu .ui-menu-item {
background-color: #eeeeee;
.ui-menu .ui-menu-item{
background-color:#eee
}
.ui-menu .ui-menu-item-alternate{
background-color: #ffffff;
background-color:#fff
}
.ui-menu a.ui-state-hover{
background: none;
background-color: #0A246A;
color: #ffffff;
}
background:none;
background-color:#0A246A;
color:#fff
}

File diff suppressed because it is too large Load diff

View file

@ -1,243 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="droid_sans_monoregular" horiz-adv-x="1229" >
<font-face units-per-em="2048" ascent="1638" descent="-410" />
<missing-glyph horiz-adv-x="500" />
<glyph horiz-adv-x="0" />
<glyph unicode="&#xd;" />
<glyph unicode=" " />
<glyph unicode="!" d="M487 111q0 139 127 139t127 -139q0 -140 -127 -140t-127 140zM504 1462h223l-51 -1048h-121z" />
<glyph unicode="&#x22;" d="M285 1462h237l-41 -528h-155zM707 1462h237l-41 -528h-155z" />
<glyph unicode="#" d="M45 428v137h244l65 328h-233v137h258l82 432h147l-82 -432h293l84 432h144l-84 -432h221v-137h-248l-64 -328h240v-137h-266l-82 -428h-148l84 428h-290l-82 -428h-144l78 428h-217zM436 565h291l64 328h-291z" />
<glyph unicode="$" d="M182 172v172q196 -92 365 -92v434q-193 64 -270.5 145.5t-77.5 219.5q0 131 92 217t256 106v180h137v-176q187 -9 336 -78l-66 -145q-140 62 -270 72v-422q199 -68 279.5 -148.5t80.5 -210.5q0 -280 -360 -335v-230h-137v221q-228 0 -365 70zM375 1049q0 -78 40.5 -121 t131.5 -74v369q-172 -29 -172 -174zM684 262q184 28 184 184q0 127 -184 189v-373z" />
<glyph unicode="%" d="M0 1133q0 164 79 255t216 91q129 0 210 -93t81 -253q0 -162 -78.5 -255.5t-216.5 -93.5q-131 0 -211 94.5t-80 254.5zM152 1133q0 -230 141 -230q139 0 139 230q0 225 -139 225q-141 0 -141 -225zM170 0l729 1462h158l-729 -1462h-158zM643 330q0 164 79 255t216 91 q129 0 210 -93t81 -253q0 -161 -78.5 -254.5t-216.5 -93.5q-130 0 -210.5 94.5t-80.5 253.5zM795 330q0 -230 141 -230q139 0 139 230q0 225 -139 225q-141 0 -141 -225z" />
<glyph unicode="&#x26;" d="M61 381q0 131 63.5 228.5t235.5 199.5q-163 192 -163 356q0 148 95.5 234t274.5 86q168 0 260.5 -85.5t92.5 -234.5q0 -203 -318 -389l281 -348q71 121 104 266h184q-53 -234 -178 -403l234 -291h-217l-131 166q-174 -186 -412 -186q-189 0 -297.5 107t-108.5 294z M252 387q0 -105 67 -175.5t160 -70.5q160 0 299 146l-323 401q-203 -123 -203 -301zM375 1165q0 -118 133 -268q134 79 183.5 138.5t49.5 133.5t-49.5 120t-130.5 46q-87 0 -136.5 -44.5t-49.5 -125.5z" />
<glyph unicode="'" d="M496 1462h237l-41 -528h-155z" />
<glyph unicode="(" d="M295 567q0 532 444 895h193q-449 -376 -449 -893q0 -521 447 -893h-191q-444 353 -444 891z" />
<glyph unicode=")" d="M297 1462h192q445 -361 445 -895q0 -540 -445 -891h-190q446 371 446 893q0 518 -448 893z" />
<glyph unicode="*" d="M133 1081l29 193l391 -111l-43 393h205l-43 -393l397 111l27 -193l-379 -28l246 -326l-179 -96l-176 358l-157 -358l-185 96l242 326z" />
<glyph unicode="+" d="M152 647v150h387v389h149v-389h387v-150h-387v-385h-149v385h-387z" />
<glyph unicode="," d="M440 -289q76 323 111 551h219l16 -24q-57 -224 -194 -527h-152z" />
<glyph unicode="-" d="M285 465v168h659v-168h-659z" />
<glyph unicode="." d="M463 135q0 166 151 166q152 0 152 -166t-152 -166q-151 0 -151 166z" />
<glyph unicode="/" d="M211 0l627 1462h178l-627 -1462h-178z" />
<glyph unicode="0" d="M147 733q0 752 465 752q232 0 350.5 -193.5t118.5 -558.5q0 -753 -469 -753q-229 0 -347 193.5t-118 559.5zM332 733q0 -318 67.5 -458t212.5 -140q147 0 216 140.5t69 457.5q0 315 -69 455.5t-216 140.5q-145 0 -212.5 -139.5t-67.5 -456.5z" />
<glyph unicode="1" d="M225 1163l383 299h150v-1462h-176v913q0 122 8 361q-39 -43 -121 -113l-147 -121z" />
<glyph unicode="2" d="M158 0v156l350 381q195 213 257 317.5t62 230.5q0 113 -63.5 178.5t-171.5 65.5q-160 0 -318 -137l-102 119q192 172 422 172q194 0 306 -106t112 -285q0 -119 -59 -244t-291 -375l-281 -299v-8h688v-166h-911z" />
<glyph unicode="3" d="M131 59v170q184 -96 383 -96q354 0 354 289q0 258 -381 258h-133v151h133q160 0 248 76t88 201q0 102 -68 161.5t-182 59.5q-178 0 -344 -121l-92 125q187 150 436 150q206 0 321.5 -99t115.5 -264q0 -138 -82 -231t-234 -119v-6q361 -47 361 -348q0 -203 -138 -319.5 t-399 -116.5q-242 0 -387 79z" />
<glyph unicode="4" d="M61 328v159l664 983h188v-976h213v-166h-213v-328h-176v328h-676zM240 494h497v356q0 176 13 432h-9q-36 -102 -90 -180z" />
<glyph unicode="5" d="M172 59v172q144 -96 360 -96q336 0 336 314q0 294 -344 294q-89 0 -231 -26l-90 57l55 688h690v-166h-532l-39 -419q107 20 209 20q210 0 339.5 -114.5t129.5 -313.5q0 -234 -138 -361.5t-389 -127.5q-224 0 -356 79z" />
<glyph unicode="6" d="M154 625q0 858 639 858q104 0 172 -19v-155q-71 24 -166 24q-224 0 -336.5 -141t-122.5 -447h12q95 170 307 170q195 0 305.5 -119t110.5 -325q0 -228 -120.5 -359.5t-323.5 -131.5q-218 0 -347.5 168.5t-129.5 476.5zM336 506q0 -152 81 -262.5t212 -110.5 q127 0 198.5 88t71.5 250q0 145 -68.5 223t-195.5 78t-213 -81t-86 -185z" />
<glyph unicode="7" d="M143 1296v166h940v-145l-555 -1317h-194l563 1296h-754z" />
<glyph unicode="8" d="M156 373q0 257 282 393q-235 146 -235 369q0 161 116.5 255.5t294.5 94.5q184 0 298 -95t114 -257q0 -230 -262 -359q309 -160 309 -393q0 -181 -126 -291t-333 -110q-216 0 -337 103.5t-121 289.5zM334 371q0 -240 276 -240q136 0 210.5 65.5t74.5 182.5q0 90 -63 159.5 t-214 143.5l-30 14q-254 -121 -254 -325zM381 1126q0 -90 49.5 -152.5t185.5 -125.5q232 103 232 278q0 101 -63 154t-173 53q-106 0 -168.5 -53t-62.5 -154z" />
<glyph unicode="9" d="M154 991q0 228 120.5 360t323.5 132q220 0 348.5 -171t128.5 -474q0 -858 -639 -858q-105 0 -172 18v156q67 -25 166 -25q224 0 336.5 141t122.5 447h-12q-96 -170 -308 -170q-194 0 -304.5 118t-110.5 326zM330 991q0 -145 68.5 -223t195.5 -78t213 81.5t86 184.5 q0 151 -80 262t-213 111q-128 0 -199 -87t-71 -251z" />
<glyph unicode=":" d="M487 111q0 139 127 139t127 -139q0 -140 -127 -140t-127 140zM487 987q0 139 127 139t127 -139t-127 -139t-127 139z" />
<glyph unicode=";" d="M410 -264q62 255 100 502h199l14 -23q-51 -197 -176 -479h-137zM494 987q0 139 127 139t127 -139t-127 -139t-127 139z" />
<glyph unicode="&#x3c;" d="M152 672v102l923 451v-160l-715 -342l715 -342v-160z" />
<glyph unicode="=" d="M152 442v150h923v-150h-923zM152 852v149h923v-149h-923z" />
<glyph unicode="&#x3e;" d="M152 221v160l714 342l-714 342v160l923 -451v-102z" />
<glyph unicode="?" d="M168 1386q209 97 426 97q214 0 339.5 -97.5t125.5 -261.5q0 -133 -53.5 -216t-196.5 -191q-118 -87 -151 -141t-33 -144v-18h-160v37q0 120 46 195t161 157q130 97 171.5 155t41.5 158q0 92 -74.5 149.5t-206.5 57.5q-174 0 -371 -90zM426 111q0 139 127 139t127 -139 q0 -140 -127 -140t-127 140z" />
<glyph unicode="@" d="M31 602q0 397 167 628.5t455 231.5q248 0 396.5 -196t148.5 -535q0 -231 -64 -370.5t-178 -139.5q-128 0 -157 180h-4q-74 -180 -230 -180q-120 0 -190 104.5t-70 280.5q0 206 97 332.5t255 126.5q137 0 256 -47l-22 -416q-2 -38 -2 -69v-7q0 -176 72 -176q100 0 100 383 q0 278 -111 436t-299 158q-226 0 -352.5 -192t-126.5 -527q0 -312 129.5 -482.5t368.5 -170.5q181 0 346 78v-133q-158 -82 -352 -82q-298 0 -465.5 207t-167.5 577zM465 602q0 -252 127 -252q131 0 145 312l15 253q-54 21 -103 21q-89 0 -136.5 -93.5t-47.5 -240.5z" />
<glyph unicode="A" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183z" />
<glyph unicode="B" d="M135 0v1462h440q274 0 397.5 -87.5t123.5 -282.5q0 -128 -78.5 -213t-212.5 -103v-10q332 -54 332 -342q0 -198 -127.5 -311t-347.5 -113h-527zM322 158h307q311 0 311 274q0 254 -324 254h-294v-528zM322 842h284q156 0 227.5 55t71.5 182q0 120 -76.5 172t-242.5 52 h-264v-461z" />
<glyph unicode="C" d="M129 733q0 346 179 548t489 202q220 0 383 -86l-78 -156q-154 78 -305 78q-215 0 -343 -158.5t-128 -429.5q0 -287 120 -437.5t351 -150.5q130 0 327 58v-162q-146 -59 -358 -59q-309 0 -473 196t-164 557z" />
<glyph unicode="D" d="M135 0v1462h342q317 0 493.5 -189t176.5 -528q0 -361 -183 -553t-528 -192h-301zM322 160h96q532 0 532 579q0 564 -493 564h-135v-1143z" />
<glyph unicode="E" d="M217 0v1462h842v-164h-656v-452h617v-162h-617v-520h656v-164h-842z" />
<glyph unicode="F" d="M244 0v1462h841v-164h-655v-516h617v-164h-617v-618h-186z" />
<glyph unicode="G" d="M117 733q0 351 162 550.5t446 199.5q192 0 346 -86l-72 -162q-146 84 -280 84q-192 0 -299 -155.5t-107 -432.5q0 -588 394 -588q97 0 202 29v436h-237v164h422v-717q-195 -75 -420 -75q-262 0 -409.5 199.5t-147.5 553.5z" />
<glyph unicode="H" d="M135 0v1462h187v-616h585v616h187v-1462h-187v682h-585v-682h-187z" />
<glyph unicode="I" d="M225 0v123l295 20v1176l-295 20v123h776v-123l-294 -20v-1176l294 -20v-123h-776z" />
<glyph unicode="J" d="M137 39v166q162 -62 309 -62q161 0 254 79.5t93 226.5v1013h186v-1011q0 -216 -140.5 -343.5t-377.5 -127.5q-217 0 -324 59z" />
<glyph unicode="K" d="M211 0v1462h186v-731l121 168l453 563h209l-521 -637l539 -825h-211l-450 698l-140 -114v-584h-186z" />
<glyph unicode="L" d="M233 0v1462h187v-1296h635v-166h-822z" />
<glyph unicode="M" d="M113 0v1462h247l248 -1192h6l250 1192h252v-1462h-153v887q0 102 14 391h-8l-283 -1278h-154l-278 1280h-8q18 -266 18 -406v-874h-151z" />
<glyph unicode="N" d="M135 0v1462h213l578 -1204h6q-14 303 -14 404v800h174v-1462h-215l-580 1210h-8q18 -277 18 -417v-793h-172z" />
<glyph unicode="O" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588z" />
<glyph unicode="P" d="M176 0v1462h404q514 0 514 -428q0 -219 -138.5 -342t-402.5 -123h-191v-569h-186zM362 727h170q198 0 283.5 72t85.5 225q0 279 -338 279h-201v-576z" />
<glyph unicode="Q" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5q0 -526 -285 -694q85 -180 279 -311l-121 -142q-236 171 -328 400q-36 -6 -76 -6q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588z" />
<glyph unicode="R" d="M186 0v1462h357q520 0 520 -415q0 -287 -289 -392l397 -655h-219l-350 604h-229v-604h-187zM373 762h164q171 0 252 66t81 210q0 141 -80 203t-258 62h-159v-541z" />
<glyph unicode="S" d="M141 49v178q211 -86 414 -86q350 0 350 240q0 104 -70.5 160t-285.5 133q-208 73 -298.5 174.5t-90.5 263.5q0 174 128.5 272.5t352.5 98.5q229 0 416 -78l-64 -164q-193 78 -360 78q-293 0 -293 -209q0 -102 65.5 -164t270.5 -133q246 -88 328.5 -181.5t82.5 -240.5 q0 -192 -138.5 -301.5t-393.5 -109.5q-265 0 -414 69z" />
<glyph unicode="T" d="M102 1298v164h1022v-164h-417v-1298h-187v1298h-418z" />
<glyph unicode="U" d="M125 520v942h186v-932q0 -387 307 -387q295 0 300 389v932h186v-948q0 -260 -126 -398t-370 -138q-483 0 -483 540z" />
<glyph unicode="V" d="M33 1462h196l295 -927q41 -127 88 -334q31 145 93 340l292 921h199l-489 -1462h-187z" />
<glyph unicode="W" d="M2 1462h170l88 -663q23 -176 40 -378t17 -241q24 151 70 312l141 516h177l145 -521q61 -219 72 -307q2 53 65 619l70 663h170l-187 -1462h-190l-168 580q-40 138 -66 282q-32 -173 -65 -284l-156 -578h-190z" />
<glyph unicode="X" d="M53 0l453 764l-422 698h199l331 -559l334 559h191l-422 -692l457 -770h-211l-355 635l-366 -635h-189z" />
<glyph unicode="Y" d="M33 1462h203l376 -739l381 739h201l-487 -893v-569h-187v559z" />
<glyph unicode="Z" d="M102 0v145l793 1151h-772v166h981v-145l-793 -1151h813v-166h-1022z" />
<glyph unicode="[" d="M412 -324v1786h528v-149h-346v-1487h346v-150h-528z" />
<glyph unicode="\" d="M211 1462h178l627 -1462h-178z" />
<glyph unicode="]" d="M289 -174h346v1487h-346v149h528v-1786h-528v150z" />
<glyph unicode="^" d="M111 549l424 924h102l481 -924h-162l-368 735l-318 -735h-159z" />
<glyph unicode="_" d="M-16 -184h1259v-140h-1259v140z" />
<glyph unicode="`" d="M418 1548v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="a" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99 l-162 -7q-196 -8 -278 -61.5t-82 -163.5z" />
<glyph unicode="b" d="M158 0v1556h182v-376q0 -81 -8 -226h8q107 164 322 164q201 0 315.5 -149.5t114.5 -417.5q0 -270 -115 -420.5t-315 -150.5q-207 0 -322 159h-12l-37 -139h-133zM340 551q0 -227 69.5 -323.5t221.5 -96.5q272 0 272 422q0 414 -274 414q-155 0 -222 -94t-67 -322z" />
<glyph unicode="c" d="M172 543q0 281 145 428t408 147q178 0 336 -59l-62 -158q-150 57 -268 57q-371 0 -371 -413q0 -406 361 -406q157 0 321 62v-160q-134 -61 -329 -61q-257 0 -399 145.5t-142 417.5z" />
<glyph unicode="d" d="M137 547q0 270 115 420.5t315 150.5q206 0 322 -160h12q-12 129 -12 162v436h182v-1556h-147l-27 147h-8q-113 -167 -322 -167q-201 0 -315.5 149.5t-114.5 417.5zM326 545q0 -414 274 -414q152 0 218.5 88.5t70.5 286.5v41q0 227 -69.5 323.5t-221.5 96.5 q-272 0 -272 -422z" />
<glyph unicode="e" d="M133 541q0 266 135.5 421.5t362.5 155.5q212 0 338.5 -134t126.5 -357v-113h-774q8 -375 344 -375q195 0 370 76v-160q-166 -75 -364 -75q-245 0 -392 149.5t-147 411.5zM326 662h573q0 305 -272 305q-275 0 -301 -305z" />
<glyph unicode="f" d="M156 961v110l317 33v96q0 193 90 280t299 87q129 0 252 -35l-41 -143q-104 28 -207 28q-123 0 -167 -50t-44 -165v-104h389v-137h-389v-961h-182v961h-317z" />
<glyph unicode="g" d="M102 -186q0 212 240 270q-96 47 -96 154q0 112 133 192q-89 37 -140 122.5t-51 186.5q0 182 106.5 280.5t303.5 98.5q86 0 150 -20h378v-113l-196 -27q65 -85 65 -213q0 -161 -106.5 -256.5t-296.5 -95.5q-55 0 -86 6q-100 -56 -100 -133q0 -84 161 -84h187 q174 0 266 -77t92 -220q0 -377 -565 -377q-218 0 -331.5 80.5t-113.5 225.5zM274 -180q0 -174 271 -174q395 0 395 223q0 88 -50 118.5t-198 30.5h-188q-230 0 -230 -198zM367 745q0 -227 227 -227q223 0 223 230q0 239 -225 239t-225 -242z" />
<glyph unicode="h" d="M160 0v1556h182v-462l-8 -144h10q103 168 336 168q389 0 389 -401v-717h-182v707q0 260 -238 260q-307 0 -307 -398v-569h-182z" />
<glyph unicode="i" d="M197 0v123l344 20v811l-269 21v123h451v-955l352 -20v-123h-878zM526 1436q0 114 107 114q106 0 106 -114q0 -58 -31.5 -86.5t-74.5 -28.5q-107 0 -107 115z" />
<glyph unicode="j" d="M135 -303q134 -39 289 -39q117 0 182.5 57t65.5 158v1081l-420 21v123h602v-1215q0 -180 -113 -277.5t-319 -97.5q-164 0 -287 35v154zM637 1436q0 114 106 114q107 0 107 -114q0 -115 -107 -115q-106 0 -106 115z" />
<glyph unicode="k" d="M215 0v1556h180v-714l-16 -289h4l135 152l395 393h222l-494 -475l522 -623h-213l-426 504l-129 -82v-422h-180z" />
<glyph unicode="l" d="M188 0v123l344 20v1270l-268 21v122h451v-1413l352 -20v-123h-879z" />
<glyph unicode="m" d="M92 0v1098h127l27 -148h10q67 168 201 168q163 0 213 -182h6q76 182 219 182q128 0 186 -92.5t58 -308.5v-717h-162v707q0 146 -26.5 203t-88.5 57q-87 0 -126.5 -83t-39.5 -278v-606h-161v707q0 260 -125 260q-84 0 -120 -79.5t-36 -318.5v-569h-162z" />
<glyph unicode="n" d="M160 0v1098h147l27 -148h10q103 168 336 168q389 0 389 -401v-717h-182v707q0 260 -238 260q-307 0 -307 -398v-569h-182z" />
<glyph unicode="o" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM303 551q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416z" />
<glyph unicode="p" d="M158 -492v1590h147l27 -148h8q110 168 322 168q201 0 315.5 -149.5t114.5 -417.5q0 -270 -115 -420.5t-315 -150.5q-207 0 -322 159h-12q12 -129 12 -162v-469h-182zM340 551q0 -227 69.5 -323.5t221.5 -96.5q272 0 272 422q0 414 -274 414q-152 0 -218.5 -88t-70.5 -287 v-41z" />
<glyph unicode="q" d="M137 547q0 270 115 420.5t315 150.5q207 0 322 -168h8l27 148h147v-1590h-182v469q0 41 12 170h-12q-113 -167 -322 -167q-201 0 -315.5 149.5t-114.5 417.5zM326 545q0 -414 274 -414q152 0 218.5 88.5t70.5 286.5v41q0 227 -69.5 323.5t-221.5 96.5q-272 0 -272 -422z " />
<glyph unicode="r" d="M264 0v1098h148l22 -201h8q76 119 163 170t214 51q118 0 240 -45l-49 -166q-123 45 -224 45q-163 0 -251.5 -92.5t-88.5 -267.5v-592h-182z" />
<glyph unicode="s" d="M203 49v166q193 -86 370 -86q275 0 275 162q0 57 -49 99t-226 106q-233 87 -293.5 159.5t-60.5 171.5q0 136 111.5 213.5t308.5 77.5q198 0 369 -74l-60 -149q-182 72 -319 72q-236 0 -236 -133q0 -61 50.5 -97.5t232.5 -101.5q211 -77 280.5 -151.5t69.5 -182.5 q0 -150 -117 -235.5t-331 -85.5q-248 0 -375 69z" />
<glyph unicode="t" d="M139 961v94l267 49l77 287h105v-293h438v-137h-438v-637q0 -195 192 -195q93 0 240 21v-138q-130 -32 -252 -32q-362 0 -362 344v637h-267z" />
<glyph unicode="u" d="M160 381v717h182v-707q0 -260 236 -260q161 0 235 92.5t74 304.5v570h182v-1098h-147l-27 147h-10q-105 -167 -334 -167q-391 0 -391 401z" />
<glyph unicode="v" d="M82 1098h188l240 -652q85 -231 100 -325h6q7 44 101 325l239 652h189l-416 -1098h-231z" />
<glyph unicode="w" d="M-4 1098h162l98 -543q34 -186 57 -393h6q31 185 68 358l133 578h193l127 -578q43 -195 67 -358h6q31 242 60 393l102 543h158l-225 -1098h-195l-131 596l-68 330h-6l-65 -334l-135 -592h-189z" />
<glyph unicode="x" d="M96 0l414 563l-393 535h207l290 -410l291 410h207l-395 -535l413 -563h-206l-310 436l-311 -436h-207z" />
<glyph unicode="y" d="M82 1098h188l262 -654q82 -205 89 -290h6q23 112 90 292l239 652h189l-475 -1241q-67 -174 -156 -261.5t-246 -87.5q-86 0 -168 17v145q62 -12 136 -12q96 0 149.5 41t95.5 141l58 150z" />
<glyph unicode="z" d="M182 0v125l660 836h-627v137h811v-146l-647 -815h665v-137h-862z" />
<glyph unicode="{" d="M225 492v155q338 0 338 189v333q0 287 438 293v-149q-144 -2 -200 -39.5t-56 -118.5v-332q0 -207 -233 -248v-12q233 -41 233 -248v-331q0 -81 56 -118.5t200 -39.5v-150q-438 6 -438 293v334q0 189 -338 189z" />
<glyph unicode="|" d="M539 -492v2048h149v-2048h-149z" />
<glyph unicode="}" d="M227 -174q143 1 199.5 39t56.5 119v331q0 207 234 248v12q-234 41 -234 248v332q0 81 -56.5 119t-199.5 39v149q439 -6 439 -293v-333q0 -189 338 -189v-155q-338 0 -338 -189v-334q0 -287 -439 -293v150z" />
<glyph unicode="~" d="M152 586v162q99 108 247 108q100 0 248 -63q130 -56 201 -56q108 0 227 121v-162q-99 -108 -248 -108q-100 0 -247 63q-131 56 -201 56q-108 0 -227 -121z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#xa1;" d="M487 979q0 139 127 139t127 -139t-127 -139t-127 139zM502 -373l51 1049h121l51 -1049h-223z" />
<glyph unicode="&#xa2;" d="M172 743q0 494 434 568v172h137v-164q162 -3 318 -59l-62 -158q-150 57 -268 57q-371 0 -371 -414q0 -405 361 -405q153 0 321 61v-159q-122 -59 -299 -62v-200h-137v206q-434 69 -434 557z" />
<glyph unicode="&#xa3;" d="M119 0v154q200 49 200 284v213h-198v137h198v324q0 168 108.5 268.5t289.5 100.5q192 0 346 -80l-66 -144q-141 72 -272 72q-223 0 -223 -246v-295h377v-137h-377v-211q0 -196 -140 -274h748v-166h-991z" />
<glyph unicode="&#xa4;" d="M174 1065l98 98l127 -129q101 68 215 68t213 -68l129 129l99 -96l-129 -129q67 -101 67 -215q0 -120 -67 -215l127 -127l-97 -96l-129 127q-99 -66 -213 -66q-118 0 -215 68l-127 -127l-96 96l127 127q-65 95 -65 213q0 114 65 213zM375 723q0 -100 70.5 -170t168.5 -70 q100 0 172 70t72 170q0 101 -71.5 172.5t-172.5 71.5q-99 0 -169 -71t-70 -173z" />
<glyph unicode="&#xa5;" d="M78 1462h192l342 -739l346 739h191l-385 -768h240v-137h-302v-158h302v-137h-302v-262h-178v262h-301v137h301v158h-301v137h234z" />
<glyph unicode="&#xa6;" d="M539 289h149v-781h-149v781zM539 776v780h149v-780h-149z" />
<glyph unicode="&#xa7;" d="M244 55v158q171 -82 323 -82q240 0 240 141q0 57 -43 97t-195 102q-197 84 -253 159t-56 179q0 179 160 258q-160 80 -160 236q0 120 104 192t277 72q162 0 326 -72l-56 -139q-156 67 -276 67q-201 0 -201 -116q0 -54 49.5 -93t196.5 -100q157 -62 231 -139.5t74 -190.5 q0 -182 -145 -270q145 -80 145 -225q0 -138 -110.5 -218.5t-307.5 -80.5q-204 0 -323 65zM414 831q0 -73 56.5 -126.5t207.5 -116.5l35 -15q114 76 114 185q0 89 -73.5 144.5t-206.5 103.5q-133 -48 -133 -175z" />
<glyph unicode="&#xa8;" d="M330 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM705 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xa9;" d="M6 731q0 343 160.5 547.5t447.5 204.5q286 0 447.5 -204t161.5 -548q0 -345 -162 -548t-447 -203q-286 0 -447 203.5t-161 547.5zM115 731q0 -301 129.5 -472t369.5 -171t370 170.5t130 472.5t-130 472.5t-370 170.5t-369.5 -171t-129.5 -472zM248 733q0 209 110 332 t301 123q128 0 254 -62l-61 -127q-110 54 -193 54q-121 0 -186 -85.5t-65 -236.5q0 -323 251 -323q94 0 215 45v-131q-107 -50 -221 -50q-195 0 -300 123.5t-105 337.5z" />
<glyph unicode="&#xaa;" d="M276 989q0 116 85 167.5t276 57.5l125 5v10q0 133 -152 133q-112 0 -245 -51l-41 110q138 58 294 58q291 0 291 -238v-444h-110l-33 118q-104 -131 -268 -131q-222 0 -222 205zM428 987q0 -90 104 -90q105 0 167.5 55t62.5 146v20l-100 -2q-114 -2 -174 -28t-60 -101z " />
<glyph unicode="&#xab;" d="M197 526v27l309 414l117 -78l-238 -348l238 -348l-117 -78zM604 526v27l309 414l117 -78l-237 -348l237 -348l-117 -78z" />
<glyph unicode="&#xac;" d="M152 647v150h923v-535h-149v385h-774z" />
<glyph unicode="&#xad;" d="M285 465v168h659v-168h-659z" />
<glyph unicode="&#xae;" d="M6 731q0 343 160.5 547.5t447.5 204.5q286 0 447.5 -204t161.5 -548q0 -345 -162 -548t-447 -203q-286 0 -447 203.5t-161 547.5zM115 731q0 -301 129.5 -472t369.5 -171t370 170.5t130 472.5t-130 472.5t-370 170.5t-369.5 -171t-129.5 -472zM348 285v893h234 q325 0 325 -265q0 -163 -159 -233l237 -395h-178l-207 352h-94v-352h-158zM506 768h72q170 0 170 141q0 74 -42 103.5t-131 29.5h-69v-274z" />
<glyph unicode="&#xaf;" d="M-20 1556v140h1269v-140h-1269z" />
<glyph unicode="&#xb0;" d="M299 1167q0 129 92.5 222.5t222.5 93.5q129 0 222.5 -93.5t93.5 -222.5t-93 -221t-223 -92q-131 0 -223 92.5t-92 220.5zM422 1167q0 -78 56 -134t136 -56q79 0 136 56t57 134q0 81 -57.5 138t-135.5 57q-79 0 -135.5 -57.5t-56.5 -137.5z" />
<glyph unicode="&#xb1;" d="M152 0v150h923v-150h-923zM152 647v150h387v389h149v-389h387v-150h-387v-385h-149v385h-387z" />
<glyph unicode="&#xb2;" d="M348 672v102l187 199q111 118 141.5 168.5t30.5 107.5q0 115 -111 115q-81 0 -164 -76l-78 86q117 103 248 103q118 0 183 -60t65 -164q0 -66 -35 -134t-164 -197l-135 -136h363v-114h-531z" />
<glyph unicode="&#xb3;" d="M342 705v124q110 -65 203 -65q149 0 149 137q0 125 -147 125h-72v104h70q123 0 123 127q0 111 -97 111q-71 0 -161 -70l-66 86q111 93 238 93q109 0 173 -54.5t64 -148.5q0 -139 -149 -189q176 -39 176 -186q0 -116 -74.5 -180t-214.5 -64q-131 0 -215 50z" />
<glyph unicode="&#xb4;" d="M418 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xb5;" d="M180 -492v1590h182v-707q0 -260 218 -260q148 0 217 91t69 306v570h183v-1098h-148l-27 147h-10q-96 -167 -295 -167q-139 0 -213 88q6 -153 6 -240v-320h-182z" />
<glyph unicode="&#xb6;" d="M66 1042q0 259 108.5 386.5t341.5 127.5h563v-1816h-121v1657h-206v-1657h-121v819q-61 -18 -146 -18q-419 0 -419 501z" />
<glyph unicode="&#xb7;" d="M487 723q0 139 127 139t127 -139t-127 -139t-127 139z" />
<glyph unicode="&#xb8;" d="M428 -375q33 -6 80 -6q151 0 151 92q0 74 -172 113l91 176h120l-57 -115q160 -37 160 -172q0 -205 -291 -205q-34 0 -82 9v108z" />
<glyph unicode="&#xb9;" d="M367 1309l219 151h135v-788h-146v465q0 69 9 200q-41 -44 -74 -65l-70 -51z" />
<glyph unicode="&#xba;" d="M281 1133q0 162 90 254t245 92q145 0 238.5 -92.5t93.5 -253.5q0 -165 -92 -257t-244 -92q-144 0 -237.5 93t-93.5 256zM432 1133q0 -230 182 -230q181 0 181 230q0 225 -181 225q-182 0 -182 -225z" />
<glyph unicode="&#xbb;" d="M197 193l237 348l-237 348l116 78l310 -414v-27l-310 -411zM604 193l238 348l-238 348l117 78l309 -414v-27l-309 -411z" />
<glyph unicode="&#xbc;" d="M22 1309l219 151h135v-788h-146v465q0 69 9 200q-41 -44 -74 -65l-70 -51zM170 0l729 1462h158l-729 -1462h-158zM606 175v98l377 523h141v-508h84v-113h-84v-174h-143v174h-375zM751 288h230v176q0 82 6 172q-40 -73 -80 -131z" />
<glyph unicode="&#xbd;" d="M3 1309l219 151h135v-788h-146v465q0 69 9 200q-41 -44 -74 -65l-70 -51zM105 0l729 1462h158l-729 -1462h-158zM694 1v102l187 199q111 118 141.5 168.5t30.5 107.5q0 115 -111 115q-81 0 -164 -76l-78 86q117 103 248 103q118 0 183 -60t65 -164q0 -66 -35 -134 t-164 -197l-135 -136h363v-114h-531z" />
<glyph unicode="&#xbe;" d="M21 705v124q110 -65 203 -65q149 0 149 137q0 125 -147 125h-72v104h70q123 0 123 127q0 111 -97 111q-71 0 -161 -70l-66 86q111 93 238 93q109 0 173 -54.5t64 -148.5q0 -139 -149 -189q176 -39 176 -186q0 -116 -74.5 -180t-214.5 -64q-131 0 -215 50zM223 0l729 1462 h158l-729 -1462h-158zM627 175v98l377 523h141v-508h84v-113h-84v-174h-143v174h-375zM772 288h230v176q0 82 6 172q-40 -73 -80 -131z" />
<glyph unicode="&#xbf;" d="M168 -35q0 134 53.5 217t196.5 191q119 87 151.5 140.5t32.5 143.5v19h160v-37q0 -119 -44.5 -193.5t-162.5 -158.5q-129 -97 -171 -155t-42 -159q0 -93 74.5 -149.5t206.5 -56.5q181 0 370 90l62 -144q-219 -106 -422 -106q-217 0 -341 97.5t-124 260.5zM547 979 q0 139 127 139t127 -139t-127 -139t-127 139z" />
<glyph unicode="&#xc0;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM341 1886v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183z" />
<glyph unicode="&#xc1;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183zM520 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xc2;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM283 1579v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183z" />
<glyph unicode="&#xc3;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM253 1579q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115h-104zM422 618h385l-133 424q-39 124 -62 226 q-20 -99 -46 -183z" />
<glyph unicode="&#xc4;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM330 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183zM705 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xc5;" d="M33 0l483 1468h195l485 -1468h-192l-144 453h-491l-146 -453h-190zM387 1581q0 99 60.5 157t162.5 58q101 0 165.5 -59t64.5 -154q0 -98 -63.5 -157.5t-166.5 -59.5q-102 0 -162.5 58t-60.5 157zM422 618h385l-133 424q-39 124 -62 226q-20 -99 -46 -183zM498 1581 q0 -113 112 -113q50 0 81.5 30t31.5 83t-31.5 83t-81.5 30q-49 0 -80.5 -30t-31.5 -83z" />
<glyph unicode="&#xc6;" d="M0 0l338 1462h872v-164h-477v-452h438v-162h-438v-520h477v-164h-653v453h-289l-96 -453h-172zM305 618h252v680h-106z" />
<glyph unicode="&#xc7;" d="M129 733q0 346 179 548t489 202q220 0 383 -86l-78 -156q-154 78 -305 78q-215 0 -343 -158.5t-128 -429.5q0 -287 120 -437.5t351 -150.5q130 0 327 58v-162q-146 -59 -358 -59q-309 0 -473 196t-164 557zM508 -375q33 -6 80 -6q151 0 151 92q0 74 -172 113l91 176h120 l-57 -115q160 -37 160 -172q0 -205 -291 -205q-34 0 -82 9v108z" />
<glyph unicode="&#xc8;" d="M217 0v1462h842v-164h-656v-452h617v-162h-617v-520h656v-164h-842zM345 1886v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xc9;" d="M217 0v1462h842v-164h-656v-452h617v-162h-617v-520h656v-164h-842zM481 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xca;" d="M217 0v1462h842v-164h-656v-452h617v-162h-617v-520h656v-164h-842zM318 1579v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xcb;" d="M217 0v1462h842v-164h-656v-452h617v-162h-617v-520h656v-164h-842zM355 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM730 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xcc;" d="M225 0v123l295 20v1176l-295 20v123h776v-123l-294 -20v-1176l294 -20v-123h-776zM310 1886v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xcd;" d="M225 0v123l295 20v1176l-295 20v123h776v-123l-294 -20v-1176l294 -20v-123h-776zM537 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xce;" d="M225 0v123l295 20v1176l-295 20v123h776v-123l-294 -20v-1176l294 -20v-123h-776zM283 1579v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xcf;" d="M225 0v123l295 20v1176l-295 20v123h776v-123l-294 -20v-1176l294 -20v-123h-776zM332 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM707 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xd0;" d="M0 659v162h135v641h342q317 0 493.5 -189t176.5 -528q0 -361 -183 -553t-528 -192h-301v659h-135zM322 160h96q532 0 532 579q0 564 -493 564h-135v-482h380v-162h-380v-499z" />
<glyph unicode="&#xd1;" d="M135 0v1462h213l578 -1204h6q-14 303 -14 404v800h174v-1462h-215l-580 1210h-8q18 -277 18 -417v-793h-172zM258 1579q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115h-104z" />
<glyph unicode="&#xd2;" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588zM337 1886v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z " />
<glyph unicode="&#xd3;" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588zM508 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z " />
<glyph unicode="&#xd4;" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588zM283 1579v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234 l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xd5;" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM262 1579q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115h-104zM281 733 q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588z" />
<glyph unicode="&#xd6;" d="M84 735q0 750 534 750q256 0 392.5 -195.5t136.5 -556.5t-138 -557t-393 -196q-532 0 -532 755zM281 733q0 -590 335 -590q173 0 253.5 145t80.5 445q0 303 -81.5 445.5t-250.5 142.5q-337 0 -337 -588zM332 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z M707 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xd7;" d="M190 1042l105 105l317 -318l322 318l104 -103l-321 -321l319 -320l-102 -102l-322 317l-317 -315l-102 103l315 317z" />
<glyph unicode="&#xd8;" d="M80 2l121 197q-117 184 -117 536q0 750 534 750q186 0 310 -105l92 152l137 -78l-125 -201q115 -188 115 -520q0 -361 -138 -557t-393 -196q-189 0 -307 94l-92 -150zM281 733q0 -212 38 -342l515 836q-80 94 -216 94q-337 0 -337 -588zM403 229q80 -86 213 -86 q173 0 253.5 145t80.5 445q0 209 -35 328z" />
<glyph unicode="&#xd9;" d="M125 520v942h186v-932q0 -387 307 -387q295 0 300 389v932h186v-948q0 -260 -126 -398t-370 -138q-483 0 -483 540zM349 1886v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xda;" d="M125 520v942h186v-932q0 -387 307 -387q295 0 300 389v932h186v-948q0 -260 -126 -398t-370 -138q-483 0 -483 540zM494 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xdb;" d="M125 520v942h186v-932q0 -387 307 -387q295 0 300 389v932h186v-948q0 -260 -126 -398t-370 -138q-483 0 -483 540zM283 1579v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xdc;" d="M125 520v942h186v-932q0 -387 307 -387q295 0 300 389v932h186v-948q0 -260 -126 -398t-370 -138q-483 0 -483 540zM332 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM707 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xdd;" d="M33 1462h203l376 -739l381 739h201l-487 -893v-569h-187v559zM494 1579v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xde;" d="M176 0v1462h186v-252h218q514 0 514 -428q0 -219 -138.5 -342t-402.5 -123h-191v-317h-186zM362 475h170q198 0 283.5 72t85.5 225q0 279 -338 279h-201v-576z" />
<glyph unicode="&#xdf;" d="M164 0v1200q0 367 424 367q194 0 302.5 -79.5t108.5 -227.5q0 -132 -133 -249q-84 -74 -108.5 -105t-24.5 -64q0 -39 29 -69t151 -111q144 -95 191 -173t47 -176q0 -162 -100 -247.5t-283 -85.5q-182 0 -289 69v166q143 -86 277 -86q213 0 213 174q0 81 -45.5 132 t-143.5 112q-123 78 -173 139t-50 148q0 117 129 223t129 196q0 164 -227 164q-242 0 -242 -215v-1202h-182z" />
<glyph unicode="&#xe0;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99 l-162 -7q-196 -8 -278 -61.5t-82 -163.5zM333 1548v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xe1;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99 l-162 -7q-196 -8 -278 -61.5t-82 -163.5zM502 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xe2;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM291 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234 l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99l-162 -7q-196 -8 -278 -61.5t-82 -163.5z" />
<glyph unicode="&#xe3;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM264 1241q25 264 211 264q58 0 161 -56q102 -56 136 -56 q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115h-104zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99l-162 -7q-196 -8 -278 -61.5t-82 -163.5z" />
<glyph unicode="&#xe4;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99 l-162 -7q-196 -8 -278 -61.5t-82 -163.5zM342 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM717 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xe5;" d="M135 307q0 332 510 348l203 7v69q0 236 -244 236q-147 0 -328 -82l-63 137q196 96 383 96q227 0 328.5 -87t101.5 -279v-752h-131l-37 152h-8q-77 -97 -158 -134.5t-209 -37.5q-163 0 -255.5 86t-92.5 241zM324 305q0 -178 200 -178q147 0 234.5 81.5t87.5 229.5v99 l-162 -7q-196 -8 -278 -61.5t-82 -163.5zM386 1456q0 99 60.5 157t162.5 58q101 0 165.5 -59t64.5 -154q0 -98 -63.5 -157.5t-166.5 -59.5q-102 0 -162.5 58t-60.5 157zM497 1456q0 -113 112 -113q50 0 81.5 30t31.5 83t-31.5 83t-81.5 30q-49 0 -80.5 -30t-31.5 -83z" />
<glyph unicode="&#xe6;" d="M45 307q0 332 328 348l149 7v69q0 236 -139 236q-107 0 -211 -82l-57 137q131 96 276 96q188 0 246 -178q78 178 240 178q137 0 223 -135.5t86 -355.5v-113h-496q3 -192 67 -283.5t169 -91.5q111 0 233 76v-162q-107 -73 -241 -73q-222 0 -316 229q-105 -229 -301 -229 q-115 0 -185.5 86.5t-70.5 240.5zM215 305q0 -83 32 -130.5t87 -47.5q82 0 134 82.5t52 228.5v99l-88 -7q-217 -17 -217 -225zM694 662h316q0 141 -40.5 223t-111.5 82q-68 0 -113 -78t-51 -227z" />
<glyph unicode="&#xe7;" d="M172 543q0 281 145 428t408 147q178 0 336 -59l-62 -158q-150 57 -268 57q-371 0 -371 -413q0 -406 361 -406q157 0 321 62v-160q-134 -61 -329 -61q-257 0 -399 145.5t-142 417.5zM477 -375q33 -6 80 -6q151 0 151 92q0 74 -172 113l91 176h120l-57 -115 q160 -37 160 -172q0 -205 -291 -205q-34 0 -82 9v108z" />
<glyph unicode="&#xe8;" d="M133 541q0 266 135.5 421.5t362.5 155.5q212 0 338.5 -134t126.5 -357v-113h-774q8 -375 344 -375q195 0 370 76v-160q-166 -75 -364 -75q-245 0 -392 149.5t-147 411.5zM326 662h573q0 305 -272 305q-275 0 -301 -305zM374 1548v21h219q86 -178 174 -301v-27h-121 q-166 142 -272 307z" />
<glyph unicode="&#xe9;" d="M133 541q0 266 135.5 421.5t362.5 155.5q212 0 338.5 -134t126.5 -357v-113h-774q8 -375 344 -375q195 0 370 76v-160q-166 -75 -364 -75q-245 0 -392 149.5t-147 411.5zM326 662h573q0 305 -272 305q-275 0 -301 -305zM500 1241v27q94 133 174 301h219v-21 q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xea;" d="M133 541q0 266 135.5 421.5t362.5 155.5q212 0 338.5 -134t126.5 -357v-113h-774q8 -375 344 -375q195 0 370 76v-160q-166 -75 -364 -75q-245 0 -392 149.5t-147 411.5zM299 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186 q-133 -136 -211 -186h-120zM326 662h573q0 305 -272 305q-275 0 -301 -305z" />
<glyph unicode="&#xeb;" d="M133 541q0 266 135.5 421.5t362.5 155.5q212 0 338.5 -134t126.5 -357v-113h-774q8 -375 344 -375q195 0 370 76v-160q-166 -75 -364 -75q-245 0 -392 149.5t-147 411.5zM326 662h573q0 305 -272 305q-275 0 -301 -305zM348 1395q0 102 96 102t96 -102q0 -103 -96 -103 t-96 103zM723 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xec;" d="M197 0v123l344 20v811l-269 21v123h451v-955l352 -20v-123h-878zM333 1548v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xed;" d="M197 0v123l344 20v811l-269 21v123h451v-955l352 -20v-123h-878zM531 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xee;" d="M197 0v123l344 20v811l-269 21v123h451v-955l352 -20v-123h-878zM283 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xef;" d="M197 0v123l344 20v811l-269 21v123h451v-955l352 -20v-123h-878zM330 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM705 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xf0;" d="M135 477q0 234 120.5 364t334.5 130q211 0 299 -119l8 4q-58 226 -242 391l-256 -153l-73 114l217 131q-104 71 -172 109l69 123q143 -70 246 -148l227 138l74 -113l-194 -117q301 -291 301 -758q0 -277 -128.5 -435t-353.5 -158q-213 0 -345 133.5t-132 363.5zM328 471 q0 -340 288 -340q154 0 221.5 99.5t67.5 295.5q0 129 -77.5 213t-213.5 84q-286 0 -286 -352z" />
<glyph unicode="&#xf1;" d="M160 0v1098h147l27 -148h10q103 168 336 168q389 0 389 -401v-717h-182v707q0 260 -238 260q-307 0 -307 -398v-569h-182zM260 1241q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115 h-104z" />
<glyph unicode="&#xf2;" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM303 551q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416zM378 1548v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z " />
<glyph unicode="&#xf3;" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM303 551q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416zM498 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z " />
<glyph unicode="&#xf4;" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM278 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120zM303 551 q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416z" />
<glyph unicode="&#xf5;" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM254 1241q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56 q-82 0 -107 -115h-104zM303 551q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416z" />
<glyph unicode="&#xf6;" d="M115 551q0 263 135.5 415t365.5 152q218 0 357 -155t139 -412q0 -265 -137 -418t-365 -153q-216 0 -355.5 155.5t-139.5 415.5zM303 551q0 -420 311 -420q310 0 310 420q0 416 -312 416q-309 0 -309 -416zM323 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z M698 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xf7;" d="M152 647v150h923v-150h-923zM498 373q0 125 114 125q115 0 115 -125t-115 -125q-114 0 -114 125zM498 1071q0 125 114 125q115 0 115 -125t-115 -125q-114 0 -114 125z" />
<glyph unicode="&#xf8;" d="M115 551q0 266 133 416.5t368 150.5q128 0 236 -57l76 119l131 -84l-84 -131q137 -156 137 -414q0 -271 -134.5 -421t-367.5 -150q-129 0 -233 53l-76 -119l-131 84l84 131q-139 158 -139 422zM303 551q0 -180 53 -275l406 656q-66 35 -156 35q-161 0 -232 -103.5 t-71 -312.5zM467 164q61 -33 154 -33q161 0 232 105.5t71 314.5q0 164 -52 266z" />
<glyph unicode="&#xf9;" d="M160 381v717h182v-707q0 -260 236 -260q161 0 235 92.5t74 304.5v570h182v-1098h-147l-27 147h-10q-105 -167 -334 -167q-391 0 -391 401zM341 1548v21h219q86 -178 174 -301v-27h-121q-166 142 -272 307z" />
<glyph unicode="&#xfa;" d="M160 381v717h182v-707q0 -260 236 -260q161 0 235 92.5t74 304.5v570h182v-1098h-147l-27 147h-10q-105 -167 -334 -167q-391 0 -391 401zM500 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xfb;" d="M160 381v717h182v-707q0 -260 236 -260q161 0 235 92.5t74 304.5v570h182v-1098h-147l-27 147h-10q-105 -167 -334 -167q-391 0 -391 401zM291 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#xfc;" d="M160 381v717h182v-707q0 -260 236 -260q161 0 235 92.5t74 304.5v570h182v-1098h-147l-27 147h-10q-105 -167 -334 -167q-391 0 -391 401zM332 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM707 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#xfd;" d="M82 1098h188l262 -654q82 -205 89 -290h6q23 112 90 292l239 652h189l-475 -1241q-67 -174 -156 -261.5t-246 -87.5q-86 0 -168 17v145q62 -12 136 -12q96 0 149.5 41t95.5 141l58 150zM494 1241v27q94 133 174 301h219v-21q-112 -173 -272 -307h-121z" />
<glyph unicode="&#xfe;" d="M158 -492v2048h182v-458l-8 -148h8q110 168 322 168q201 0 315.5 -149.5t114.5 -417.5q0 -270 -115 -420.5t-315 -150.5q-207 0 -322 159h-12q12 -129 12 -162v-469h-182zM340 551q0 -227 69.5 -323.5t221.5 -96.5q272 0 272 422q0 414 -274 414q-152 0 -218.5 -88 t-70.5 -287v-41z" />
<glyph unicode="&#xff;" d="M82 1098h188l262 -654q82 -205 89 -290h6q23 112 90 292l239 652h189l-475 -1241q-67 -174 -156 -261.5t-246 -87.5q-86 0 -168 17v145q62 -12 136 -12q96 0 149.5 41t95.5 141l58 150zM338 1395q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM713 1395q0 102 96 102 t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#x152;" d="M20 735q0 750 512 750q82 0 154 -23h541v-164h-398v-452h359v-162h-359v-520h398v-164h-574l-48 -10q-51 -10 -97 -10q-488 0 -488 755zM209 733q0 -590 309 -590q70 0 133 33v1112q-58 33 -131 33q-311 0 -311 -588z" />
<glyph unicode="&#x153;" d="M57 551q0 268 88 417.5t242 149.5q171 0 260 -217q84 217 242 217q131 0 208 -133.5t77 -357.5v-113h-439q3 -375 189 -375q108 0 204 76v-162q-94 -73 -217 -73q-87 0 -158.5 59t-101.5 162q-88 -221 -266 -221q-146 0 -237 153.5t-91 417.5zM227 551 q0 -211 39.5 -315.5t130.5 -104.5q168 0 168 410q0 426 -170 426q-90 0 -129 -103t-39 -313zM737 662h260q0 305 -123 305q-125 0 -137 -305z" />
<glyph unicode="&#x178;" d="M33 1462h203l376 -739l381 739h201l-487 -893v-569h-187v559zM332 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103zM707 1733q0 102 96 102t96 -102q0 -103 -96 -103t-96 103z" />
<glyph unicode="&#x2c6;" d="M283 1241v27q25 29 59 67q139 155 176 234h193q37 -79 176 -234l59 -67v-27h-121q-85 56 -211 186q-133 -136 -211 -186h-120z" />
<glyph unicode="&#x2dc;" d="M254 1241q25 264 211 264q58 0 161 -56q102 -56 136 -56q80 0 106 114h105q-27 -264 -211 -264q-57 0 -157 57q-100 56 -140 56q-82 0 -107 -115h-104z" />
<glyph unicode="&#x2000;" horiz-adv-x="953" />
<glyph unicode="&#x2001;" horiz-adv-x="1907" />
<glyph unicode="&#x2002;" horiz-adv-x="953" />
<glyph unicode="&#x2003;" horiz-adv-x="1907" />
<glyph unicode="&#x2004;" horiz-adv-x="635" />
<glyph unicode="&#x2005;" horiz-adv-x="476" />
<glyph unicode="&#x2006;" horiz-adv-x="317" />
<glyph unicode="&#x2007;" horiz-adv-x="317" />
<glyph unicode="&#x2008;" horiz-adv-x="238" />
<glyph unicode="&#x2009;" horiz-adv-x="381" />
<glyph unicode="&#x200a;" horiz-adv-x="105" />
<glyph unicode="&#x2010;" d="M285 465v168h659v-168h-659z" />
<glyph unicode="&#x2011;" d="M285 465v168h659v-168h-659z" />
<glyph unicode="&#x2012;" d="M285 465v168h659v-168h-659z" />
<glyph unicode="&#x2013;" d="M184 465v168h860v-168h-860z" />
<glyph unicode="&#x2014;" d="M-6 465v168h1241v-168h-1241z" />
<glyph unicode="&#x2018;" d="M446 983q54 206 177 479h157q-62 -255 -100 -501h-219z" />
<glyph unicode="&#x2019;" d="M446 961q65 266 101 501h219l14 -22q-55 -213 -176 -479h-158z" />
<glyph unicode="&#x201a;" d="M457 -264q71 292 100 502h199l14 -23q-55 -213 -176 -479h-137z" />
<glyph unicode="&#x201c;" d="M233 983q54 206 177 479h157q-62 -255 -100 -501h-219zM659 983q54 206 177 479h157q-62 -255 -100 -501h-219z" />
<glyph unicode="&#x201d;" d="M233 961q65 266 101 501h219l14 -22q-55 -213 -176 -479h-158zM659 961q65 266 101 501h219l14 -22q-55 -213 -176 -479h-158z" />
<glyph unicode="&#x201e;" d="M244 -264q71 292 100 502h199l14 -23q-55 -213 -176 -479h-137zM670 -264q71 292 100 502h199l14 -23q-51 -197 -176 -479h-137z" />
<glyph unicode="&#x2022;" d="M379 748q0 262 235 262q236 0 236 -262q0 -128 -63.5 -195.5t-172.5 -67.5q-111 0 -173 67t-62 196z" />
<glyph unicode="&#x2026;" d="M78 111q0 139 127 139t127 -139q0 -140 -127 -140t-127 140zM487 111q0 139 127 139t127 -139q0 -140 -127 -140t-127 140zM897 111q0 139 127 139t127 -139q0 -140 -127 -140t-127 140z" />
<glyph unicode="&#x202f;" horiz-adv-x="381" />
<glyph unicode="&#x2039;" d="M401 526v27l310 414l116 -78l-237 -348l237 -348l-116 -78z" />
<glyph unicode="&#x203a;" d="M401 193l238 348l-238 348l117 78l309 -414v-27l-309 -411z" />
<glyph unicode="&#x205f;" horiz-adv-x="476" />
<glyph unicode="&#x20ac;" d="M96 502v137h148l-2 38l2 120h-148v137h160q39 262 179.5 405.5t359.5 143.5q191 0 335 -92l-79 -146q-122 74 -242 74q-136 0 -233 -99.5t-134 -285.5h432v-137h-446q0 -15 -1 -29l-1 -25v-62q0 -20 2 -42h385v-137h-367q73 -359 369 -359q136 0 268 58v-162 q-122 -59 -282 -59q-447 0 -541 522h-164z" />
<glyph unicode="&#x2122;" d="M0 1354v108h481v-108h-178v-613h-127v613h-176zM526 741v721h187l139 -534l149 534h179v-721h-127v342q0 87 10 207h-12l-154 -549h-100l-146 549h-12l10 -180v-369h-123z" />
<glyph unicode="&#x25fc;" horiz-adv-x="1100" d="M0 0v1100h1100v-1100h-1100z" />
<glyph unicode="&#xfb01;" d="M49 961v75l195 68v96q0 190 76.5 278.5t254.5 88.5q97 0 197 -37l-47 -141q-79 28 -143 28q-92 0 -124 -50.5t-32 -164.5v-104h246v-137h-246v-961h-182v961h-195zM854 1395q0 114 107 114q106 0 106 -114q0 -58 -31.5 -86.5t-74.5 -28.5q-107 0 -107 115zM868 0v1098 h183v-1098h-183z" />
<glyph unicode="&#xfb02;" d="M49 961v75l195 68v96q0 190 76.5 278.5t254.5 88.5q97 0 197 -37l-47 -141q-79 28 -143 28q-92 0 -124 -50.5t-32 -164.5v-104h246v-137h-246v-961h-182v961h-195zM868 0v1556h183v-1556h-183z" />
<glyph unicode="&#xfb03;" d="M66 971v65l100 62v82q0 106 20 176.5t58.5 112.5t94.5 59.5t128 17.5q49 0 92.5 -11t77.5 -24q38 29 89 42.5t114 13.5q52 0 97.5 -11.5t82.5 -25.5l-41 -131q-29 11 -63.5 19.5t-71.5 8.5q-38 0 -65.5 -11t-45.5 -37t-26.5 -69.5t-8.5 -107.5v-104h367v-1098h-164v971 h-203v-971h-163v971h-205v-971h-164v971h-100zM330 1098h205v102q0 115 24 193q-20 6 -42.5 10t-45.5 4q-35 0 -61.5 -11t-44 -37t-26.5 -69.5t-9 -107.5v-84z" />
<glyph unicode="&#xfb04;" d="M66 971v65l100 62v82q0 106 20 176.5t58.5 112.5t94.5 59.5t128 17.5q49 0 92.5 -11t77.5 -24q38 29 89 42.5t114 13.5q48 0 94 -11h131v-1556h-164v1421q-14 3 -28.5 4.5t-28.5 1.5q-38 0 -65.5 -11t-45.5 -37t-26.5 -69.5t-8.5 -107.5v-104h142v-127h-142v-971h-163 v971h-205v-971h-164v971h-100zM330 1098h205v102q0 115 24 193q-20 6 -42.5 10t-45.5 4q-35 0 -61.5 -11t-44 -37t-26.5 -69.5t-9 -107.5v-84z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

View file

@ -0,0 +1,45 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="sgicons" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" d="" horiz-adv-x="512" />
<glyph unicode="&#xe600;" d="M79.571-56.5h288.286v1009h-288.286v-1009zM656.143 952.5v-1009h288.285v1009h-288.285z" />
<glyph unicode="&#xe601;" d="M931.154 490.564l-789.664 455.638c-32.43 18.646-72.967-4.87-72.967-42.16v-912.084c0-37.291 40.537-60.806 72.967-42.16l789.665 455.639c32.429 19.468 32.429 66.481-0.001 85.127z" />
<glyph unicode="&#xe602;" d="M761.411 562.247c18.435 12.843 61.614 42.956 61.648 115.976l0.092 150.405 20.565-0.010c9.422-0.006 17.128 7.695 17.136 17.107l0.050 80.693c0.010 9.422-7.688 17.127-17.111 17.131l-663.030 0.373c-9.414 0.006-17.125-7.701-17.136-17.115l-0.044-80.686c-0.003-9.418 7.697-17.121 17.111-17.13l20.571-0.014-0.079-153.139c-0.040-72.294 21.969-89.236 60.867-112.973l169.176-103.22-0.025-43.981c-42.639-25.61-154.017-92.64-168.622-102.815-18.435-12.849-61.622-42.952-61.664-115.97l-0.071-129.501-20.57 0.006c-9.415 0.011-17.133-7.689-17.142-17.104l-0.038-80.688c-0.008-9.42 7.693-17.121 17.113-17.136l663.032-0.377c9.401-0.006 17.115 7.693 17.125 17.115l0.036 80.674c0.018 9.424-7.68 17.131-17.109 17.139l-20.564 0.021 0.055 132.232c0.050 72.285-21.947 89.241-60.851 112.962l-169.171 103.222 0.018 43.979c42.634 25.605 154.018 92.631 168.632 102.824zM545.877 371.844l183.96-112.222c14.113-8.614 21.657-13.372 25.134-18.894 4.184-6.622 6.199-20.184 6.184-41.428l-0.068-132.241-498.608 0.28 0.059 129.49c0.027 40.879 20.402 55.088 35.272 65.46 11.636 8.099 114.046 69.914 180.040 109.495 9.277 5.559 14.96 15.591 14.964 26.412l0.052 78.724c-0.004 10.751-5.592 20.724-14.767 26.325l-183.961 112.24c-14.121 8.609-21.657 13.365-25.137 18.888-4.181 6.628-6.202 20.173-6.187 41.428l0.083 153.139 498.622-0.279-0.092-150.407c-0.014-40.877-20.382-55.080-35.256-65.45-11.625-8.104-114.040-69.923-180.034-109.497-9.276-5.561-14.969-15.589-14.977-26.422l-0.042-78.71c-0.006-10.759 5.592-20.718 14.759-26.331z" />
<glyph unicode="&#xe603;" d="M1020.645 465.34c-1 8.425-7.896 14.915-16.289 15.447l-423.309 27.24 105.375 410.796c2.091 8.176-1.904 16.693-9.581 20.345-7.707 3.557-16.848 1.186-21.779-5.741l-117.2-164.413c-51.346-71.957-83.235-155.956-92.565-243.794l-0.812-8.362-129.901 8.362c-13.761 67.806-71.643 121.007-143.974 125.626-86.871 5.43-161.478-60.971-167.064-146.844-5.491-86.435 60.378-161.447 146.875-167.065 3.371-0.249 6.771-0.375 10.173-0.375 62.532 0 117.169 37.009 142.351 90.803l129.151-27.522-13.884-131.208c-63.624-7.645-117.044-53.67-133.239-117.076-10.453-40.597-4.399-83.002 17.006-119.167 21.406-36.072 55.573-61.847 96.232-72.206 12.854-3.308 26.023-4.993 39.16-4.993 71.799 0 134.378 48.615 152.351 118.263 17.942 70.208-14.604 141.759-74.997 175.861l32.435 126.001 8.239-1.686c86.496-18.41 176.113-14.166 260.487 12.294l192.589 60.536c8.019 2.621 13.198 10.548 12.17 18.878zM246.997 488.337c-2.996-47.367-43.56-84.5-92.082-80.88-47.648 3.059-83.969 44.31-80.943 91.988 3.028 45.496 40.971 81.131 86.528 81.131 1.872 0 3.682-0.063 5.553-0.188 47.744-3.027 84.065-44.34 80.944-92.051zM521.276 90.992c-11.576-45.434-59.349-74.297-105.468-62.377-22.437 5.741-41.281 19.846-53.077 39.816-11.763 19.876-15.134 43.247-9.392 65.652 9.86 38.382 44.371 65.216 83.875 65.216 7.302 0 14.542-0.905 21.625-2.746 46.305-11.95 74.311-59.194 62.437-105.561zM511.573 418.317c-20.813 0-37.694 16.85-37.694 37.85 0 20.813 16.881 37.788 37.694 37.788 20.921 0 37.864-16.912 37.864-37.788 0.001-21-16.943-37.85-37.864-37.85z" />
<glyph unicode="&#xe604;" d="M1020.319 639.86l-508.288 276.25-508.35-276.25 508.35-276.374zM512.031 274.444l-413.847 224.81-94.503-51.316 508.35-276.188 508.288 276.188-94.504 51.316zM512.031 82.521l-413.847 224.935-94.503-51.255 508.35-276.311 508.288 276.311-94.504 51.255z" />
<glyph unicode="&#xe605;" d="M1021.387 353.527l-509.387 395.403-509.387-395.403v161.214l509.387 395.396 509.387-395.4v-161.21zM894.040 367.903v-382.040h-254.693v254.694h-254.694v-254.693h-254.693v382.040l382.040 286.529 382.040-286.53z" />
<glyph unicode="&#xe606;" d="M841.744 833.411h-134.622v87.48c0 17.609-14.202 31.242-31.811 31.242-1.708 0-2.847-0.57-3.404-1.14-0.569 0.569-1.709 1.14-2.278 1.14h-320.942c-17.607 0-31.241-13.633-31.241-31.242v-87.482h-135.19c-38.633 0-68.736-30.102-68.736-68.735v-112.47h59.646v-640.171c0-38.635 29.533-68.166 68.167-68.166h541.337c38.633 0 68.733 29.531 68.733 68.166v640.172h59.076v112.47c0 38.633-30.113 68.736-68.735 68.736zM379.929 889.651h264.143v-56.24h-264.143v56.24z" />
<glyph unicode="&#xe607;" d="M654.205 664.255v287.358h-573.241v-1007.226h862.073v719.868h-288.832zM576.932 144.171l-56.112-56.111-124.225 124.193-124.207-124.193-56.142 56.111 124.209 124.222-124.209 124.211 56.142 56.142 124.208-124.208 124.224 124.208 56.111-56.142-124.193-124.211 124.194-124.222zM896.818 705.204h-192.409v192.409z" />
<glyph unicode="&#xe608;" d="M654.205 664.255v287.358h-573.241v-1007.226h862.073v719.868h-288.832zM896.818 705.204h-192.409v192.409z" />
<glyph unicode="&#xe609;" d="M3.543 770.037v-474.601c0-69.889 65.419-135.554 135.555-135.554h406.79l305.063-169.507v169.508h33.889c70.198 0 135.617 65.665 135.617 135.554v474.6c0 69.889-65.419 135.587-135.617 135.587h-745.742c-70.136 0-135.555-65.698-135.555-135.587zM715.394 532.847c0 37.613 30.353 68.089 67.778 68.089 37.429 0 67.779-30.476 67.779-68.089 0-37.583-30.351-68.057-67.779-68.057-37.426 0-67.778 30.474-67.778 68.057zM444.222 532.847c0 37.613 30.352 68.089 67.777 68.089 37.427 0 67.777-30.476 67.777-68.089 0-37.583-30.351-68.057-67.777-68.057s-67.777 30.474-67.777 68.057zM172.986 532.847c0 37.613 30.413 68.089 67.841 68.089 37.426 0 67.84-30.476 67.84-68.089 0-37.583-30.414-68.057-67.84-68.057-37.428 0-67.841 30.474-67.841 68.057z" />
<glyph unicode="&#xe60a;" d="M1000.179 878.501h-524.541c-8.605 0-20.678 4.764-26.97 10.639l-56.416 52.722c-6.29 5.875-18.362 10.638-26.97 10.638h-341.461c-8.607 0-15.583-6.975-15.583-15.583v-672.532c0-8.604 6.975-15.583 15.583-15.583h976.357c8.607 0 15.583 6.978 15.583 15.583v598.532c0.001 8.608-6.975 15.584-15.582 15.584zM617.27 72.229c0 13.855-8.145 25.009-18.163 25.009h-41.021v106.773c0 13.832-8.11 25.016-18.15 25.016h-56.41c-10.015 0-18.15-11.214-18.15-25.016v-106.773h-47.826c-10.013 0-18.149-11.195-18.149-25.009v-28.715h-281.901v-46.078h281.879v-27.944c0-13.819 8.135-24.99 18.149-24.99h181.572c10.025 0 18.167 11.189 18.167 24.99v27.944h283.233v46.078h-283.233v28.715h0.003z" />
<glyph unicode="&#xe60b;" d="M1000.179 878.501h-524.541c-8.606 0-20.678 4.764-26.97 10.639l-56.416 52.722c-6.29 5.875-18.362 10.638-26.97 10.638h-341.461c-8.607 0-15.583-6.975-15.583-15.583v-672.532c0-8.604 6.975-15.583 15.583-15.583h976.357c8.607 0 15.583 6.977 15.583 15.583v598.532c0.001 8.608-6.975 15.584-15.582 15.584zM699.115 442.482l-75.656-74.982-111.46 110.468-111.456-110.468-75.66 74.982 111.459 110.467-111.451 110.46 75.657 74.984 111.451-110.46 111.46 110.466 75.658-74.984-111.461-110.466 111.459-110.467zM617.27 72.228c0 13.856-8.145 25.009-18.164 25.009h-41.021v106.774c0 13.832-8.11 25.015-18.15 25.015h-56.411c-10.014 0-18.15-11.214-18.15-25.015v-106.774h-47.826c-10.013 0-18.15-11.195-18.15-25.009v-28.714h-281.898v-46.078h281.879v-27.945c0-13.819 8.135-24.99 18.149-24.99h181.572c10.025 0 18.167 11.189 18.167 24.99v27.945h283.233v46.078h-283.233v28.714h0.003z" />
<glyph unicode="&#xe60c;" d="M834.829 954.312h-645.659c-102.114 0-184.858-82.765-184.858-184.858v-642.887c0-102.113 82.744-184.878 184.858-184.878h645.659c102.071 0 184.858 82.786 184.858 184.878v642.887c0.001 102.093-82.764 184.858-184.858 184.858zM840.999 534.333l-349.249-349.228c-14.833-14.854-34.913-23.152-55.873-23.152-20.961 0-41.060 8.341-55.872 23.152l-173.1 173.079c-30.849 30.87-30.849 80.896 0.021 111.788 14.812 14.791 34.89 23.131 55.851 23.131s41.039-8.34 55.851-23.131l103.146-103.188c3.741-3.741 8.835-5.849 14.124-5.849s10.362 2.107 14.103 5.849l279.276 279.296c14.831 14.79 34.91 23.132 55.893 23.132 20.917 0 41.039-8.342 55.829-23.132 30.871-30.872 30.871-80.896 0-111.747z" />
<glyph unicode="&#xe60d;" d="M965.152 951.152h-906.304c-27.614 0-50-22.386-50-50v-906.304c0-27.613 22.386-50 50-50h906.304c27.613 0 50 22.387 50 50v906.304c0 27.614-22.386 50-50 50zM356.059 192.624c-47.102-14.466-102.444-8.501-141.803 8.784-75.499 33.156-116.545 107.414-125.487 207.683-9.78 109.65 20.917 202.954 77.175 254.113 22.65 20.598 47.063 35.085 81.567 43.294 11.48 2.73 25.021 4.994 38.901 5.020 132.548 0.243 189.64-82.735 202.036-204.546-41.62 0-83.24 0-124.86 0-3.664 46.675-26.089 87.036-79.059 81.567-16.156-1.668-30.932-13.169-38.901-23.843-25.836-34.603-30.441-101.229-25.726-158.115 4.753-57.343 25.715-105.981 85.959-99.762 40.313 4.16 53.765 43.904 58.98 84.704 42.038 0 84.077 0 126.116 0-11.641-102.061-51.284-173.218-134.898-198.899zM802.131 192.624c-47.303-14.529-102.175-8.236-141.802 8.784-75.602 32.474-116.367 108.091-125.487 207.683-12.525 136.772 36.54 246.848 127.37 287.368 10.077 4.496 20.244 7.494 31.371 10.039 11.907 2.723 25.084 4.974 38.9 5.020 131.709 0.434 190.631-83.542 202.038-204.546-41.62 0-83.241 0-124.86 0-3.106 41.457-21.030 77.309-60.862 81.567-13.108 1.401-25.999-0.603-35.765-5.647-41.414-21.39-51.308-94.612-48.313-157.486 2.9-60.906 17.737-125.759 87.214-118.586 40.046 4.132 53.515 42.376 58.979 84.704 42.038 0 84.078 0 126.116 0-10.646-99.789-50.791-173.068-134.899-198.9z" />
<glyph unicode="&#xe60e;" d="M470.14 419.703c17.361 41.131 32.048 73.378 44.115 96.741 9.053 17.361 17.564 31.976 25.465 43.877 7.935 11.868 17.529 23.465 28.856 34.79 11.325 11.291 24.345 19.802 39.063 25.465 14.682 5.663 31.093 8.478 49.199 8.478h144.856v-108.642c0-4.883 1.763-9.156 5.358-12.716 3.594-3.595 7.831-5.391 12.715-5.391 5.289 0 9.629 1.695 13.021 5.085l181.036 181.071c3.426 3.391 5.121 7.731 5.121 13.021 0 5.256-1.695 9.595-5.121 13.020l-180.461 180.459c-4.542 3.797-9.087 5.662-13.597 5.662-5.29 0-9.596-1.695-13.021-5.086-3.356-3.39-5.052-7.731-5.052-13.020v-108.609h-144.854c-25.633 0-49.777-3.12-72.427-9.358-22.618-6.206-42.727-14.14-60.255-23.77-17.535-9.596-34.52-22.515-50.926-38.757-16.417-16.208-30.348-31.874-41.852-46.963-11.491-15.055-23.592-33.942-36.215-56.56-12.648-22.616-22.82-42.453-30.547-59.408-7.736-16.987-17.056-37.909-28.012-62.797-17.361-41.13-32.074-73.379-44.148-96.774-9.020-17.328-17.528-31.942-25.424-43.843-7.935-11.868-17.573-23.465-28.865-34.79-11.321-11.334-24.342-19.802-39.062-25.466-14.678-5.662-31.095-8.478-49.193-8.478h-126.753c-5.285 0-9.626-1.695-13.020-5.085-3.396-3.398-5.086-7.74-5.086-13.021v-108.649c0-5.282 1.729-9.622 5.12-13.021 3.386-3.384 7.727-5.087 13.021-5.087h126.72c25.669 0 49.808 3.129 72.42 9.368 22.625 6.196 42.729 14.14 60.264 23.771 17.527 9.587 34.514 22.514 50.926 38.758 16.406 16.207 30.348 31.874 41.877 46.929 11.491 15.089 23.566 33.942 36.181 56.593 12.647 22.617 22.82 42.418 30.555 59.407 7.725 16.987 17.079 37.9 28.002 62.796v0zM23.16 629.054h126.753c16.581 0 31.938-2.747 46.077-8.206 14.174-5.459 26.042-12.071 35.667-19.802 9.601-7.731 19.227-18.379 28.865-31.976 9.593-13.563 17.222-25.567 22.879-35.909 5.667-10.375 12.649-24.075 20.956-41.029 29.030 68.258 54.869 119.764 77.519 154.452-60.357 84.873-137.702 127.292-231.963 127.292h-126.753c-5.285 0-9.626-1.695-13.020-5.086-3.396-3.39-5.086-7.731-5.086-13.021v-108.607c0-5.29 1.69-9.63 5.086-13.020 3.395-3.393 7.736-5.088 13.020-5.088v0zM833.365 387.991c-4.543 3.806-9.054 5.671-13.598 5.671-5.29 0-9.629-1.703-12.987-5.087-3.391-3.391-5.086-7.73-5.086-13.020v-108.643h-144.855c-16.614 0-31.975 2.737-46.113 8.23-14.141 5.468-26.044 12.045-35.64 19.811-9.628 7.732-19.226 18.379-28.855 31.975-9.629 13.563-17.259 25.534-22.923 35.909-5.661 10.377-12.647 24.075-20.92 41.021-29.064-67.877-54.693-119.383-76.934-154.452 10.172-14.707 20.709-27.839 31.666-39.326 10.919-11.494 21.29-21.598 31.128-30.246 9.799-8.68 20.921-16.242 33.365-22.659 12.444-6.4 23.396-11.756 32.823-16.098 9.461-4.341 21.329-7.833 35.639-10.479 14.343-2.646 26.424-4.611 36.215-5.942 9.799-1.313 23.193-2.263 40.181-2.84 16.988-0.543 30.722-0.745 41.301-0.543 10.546 0.17 25.838 0.372 45.844 0.543 19.972 0.204 36.012 0.298 48.082 0.298v-108.634c0-4.926 1.798-9.155 5.358-12.716 3.594-3.594 7.832-5.392 12.75-5.392 5.29 0 9.63 1.695 12.986 5.086l181.072 181.070c3.39 3.392 5.086 7.698 5.086 12.987s-1.696 9.63-5.121 13.022l-180.464 180.454z" />
<glyph unicode="&#xe60f;" d="M512 954.233c-279.665 0-506.233-226.568-506.233-506.233 0-279.667 226.568-506.234 506.234-506.234s506.234 226.567 506.234 506.234c-0.001 279.665-226.568 506.233-506.235 506.233zM750.713 494.942l-118.485-111.726 36.172-129.643c7.268-26.198-5.577-36.176-29.071-22.147l-127.279 76.74-127.445-76.567c-23.326-14.029-37.016-4.055-30.424 22.481l34.987 140.966-114.771 101.248c-20.451 18.086-14.873 32.622 12.339 32.622h139.448l66.257 154.151c10.817 25.016 28.058 25.016 38.536-0.168l64.737-153.983h141.309c27.38-0.169 33.469-15.382 13.69-33.974z" />
<glyph unicode="&#xe610;" d="M796.709 321.464v126.536h-316.344v126.536h316.344v126.538l189.807-189.806-189.807-189.804zM733.439 384.73v-253.072h-316.341v-189.808l-379.613 189.808v822.49h695.957v-316.344h-63.27v253.076h-506.151l253.076-126.538v-569.417h253.076v189.805h63.266z" />
<glyph unicode="&#xe611;" d="M1014.712 707.832c-0.774 9.002-6.706 16.736-15.173 19.819-8.493 3.102-17.997 0.99-24.387-5.388l-130.711-130.701-144.898 45.881-45.879 144.899 130.708 130.71c6.398 6.391 8.485 15.898 5.402 24.375-3.114 8.495-10.828 14.411-19.851 15.19-79.38 6.84-157.158-21.502-213.395-77.746-77.018-77.026-97.415-189.503-61.296-285.378-3.948-3.325-7.843-6.849-11.685-10.649l-436.31-411.707c-0.153-0.15-0.304-0.314-0.471-0.462-51.313-51.314-51.313-134.805 0-186.129 51.319-51.309 134.2-50.683 185.502 0.631 0.221 0.207 0.424 0.412 0.626 0.644l408.113 439.708c3.722 3.736 7.176 7.645 10.432 11.657 95.899-36.181 208.451-15.835 285.538 61.244 56.221 56.244 84.585 134.031 77.735 213.402zM166.52 35.98c-18.908-18.903-49.531-18.89-68.437 0-18.9 18.887-18.9 49.535 0 68.426 18.907 18.892 49.529 18.892 68.437 0 18.9-18.89 18.9-49.539 0-68.426z" />
<glyph unicode="&#xe612;" d="M998.781 896.127h-671.069c-8.009 0-14.526-6.515-14.526-14.526v-169.954c0-8.009 6.517-14.525 14.526-14.525h671.069c8.007 0 14.526 6.517 14.526 14.525v169.954c0.001 8.011-6.519 14.526-14.526 14.526zM998.781 547.503h-671.069c-8.009 0-14.526-6.514-14.526-14.525v-169.954c0-8.010 6.517-14.526 14.526-14.526h671.069c8.007 0 14.526 6.517 14.526 14.526v169.954c0.001 8.011-6.519 14.525-14.526 14.525zM998.781 198.88h-671.069c-8.009 0-14.526-6.515-14.526-14.526v-169.954c0-8.009 6.517-14.526 14.526-14.526h671.069c8.007 0 14.526 6.518 14.526 14.526v169.954c0.001 8.011-6.519 14.526-14.526 14.526zM209.029 896.127h-194.134c-2.317 0-4.203-6.515-4.203-14.526v-169.954c0-8.009 1.885-14.525 4.203-14.525h194.134c2.316 0 4.203 6.517 4.203 14.525v169.954c-0.001 8.011-1.887 14.526-4.203 14.526zM209.029 547.503h-194.134c-2.317 0-4.203-6.514-4.203-14.525v-169.954c0-8.010 1.885-14.526 4.203-14.526h194.134c2.316 0 4.203 6.517 4.203 14.526v169.954c-0.001 8.011-1.887 14.525-4.203 14.525zM209.029 198.88h-194.134c-2.317 0-4.203-6.515-4.203-14.526v-169.954c0-8.009 1.885-14.526 4.203-14.526h194.134c2.316 0 4.203 6.518 4.203 14.526v169.954c-0.001 8.011-1.887 14.526-4.203 14.526z" />
<glyph unicode="&#xe613;" d="M867.308 308.343v-232.27h-710.585v232.27h-136.585v-300.547c0-37.738 30.538-68.278 68.403-68.278h846.92c37.864 0 68.402 30.54 68.402 68.278v300.547h-136.555zM499.539 331.434l-195.553 236.238c0 0-29.733 28.056 2.514 28.056 32.276 0 110.174 0 110.174 0s0 18.932 0 48.043c0 83.113 0 234.316 0 295.951 0 0-4.376 16.759 20.887 16.759 25.417 0 136.896 0 155.021 0 18.25 0 17.876-14.152 17.876-14.152 0-59.712 0-216.128 0-296.511 0-26.070 0-42.953 0-42.953s62.445 0 101.673 0c39.104 0 9.684-29.421 9.684-29.421s-166.35-220.847-189.596-243.997c-16.665-16.76-32.68 1.987-32.68 1.987z" />
<glyph unicode="&#xe614;" d="M137.958 384.785h-131.676v-316.074l93.618 86.057c91.735-128.591 242.117-212.486 412.1-212.486 257.921 0 470.654 193.101 501.768 442.504h-127.727c-30.063-179.396-186.124-316.075-374.041-316.075-133.128 0-250.329 68.647-317.957 172.483l156.185 143.591h-212.27zM512 953.718c-257.921 0-470.685-193.070-501.798-442.503h127.756c30.096 179.397 186.095 316.074 374.042 316.074 137.109 0 257.304-72.752 323.913-181.804l-134.268-134.27h316.075v316.074l-90.502-90.532c-91.366 131.183-243.23 216.961-415.218 216.961z" />
<glyph unicode="&#xe615;" d="M7.43 953.884v-1011.769h1009.14v1011.769h-1009.14zM385.859 303.463v216.807h252.282v-216.806l-252.282-0.001zM638.142 231.193v-216.808h-252.283v216.808h252.283zM638.142 809.345v-216.807h-252.283v216.807h252.283zM322.787 809.345v-216.807h-252.286v216.807h252.286zM70.501 520.27h252.285v-216.806h-252.285v216.806zM701.214 520.27h252.287v-216.806h-252.287v216.806zM701.214 592.538v216.807h252.287v-216.807h-252.287zM70.501 231.193h252.285v-216.808h-252.285v216.808zM701.214 14.386v216.807h252.287v-216.808l-252.287 0.001z" />
<glyph unicode="&#xe616;" d="M765.143 574.573c0 209.706-170.016 379.723-379.712 379.723-209.71 0-379.727-170.017-379.727-379.723 0-209.699 170.017-379.717 379.726-379.717 209.696 0.001 379.713 170.018 379.713 379.717v0zM385.429 289.783c-157.050 0-284.801 127.751-284.801 284.791 0 157.047 127.75 284.798 284.801 284.798 157.038 0 284.788-127.75 284.788-284.798 0-157.041-127.75-284.791-284.788-284.791v0zM990.484 103.754l-232.322 232.324c-34.611-53.837-80.403-99.629-134.24-134.24l232.324-232.322c37.082-37.082 97.215-37.082 134.238 0 37.082 37.023 37.082 97.156 0 134.238v0z" />
<glyph unicode="&#xe617;" d="M511.895 956.466c-280.739 0-508.361-227.622-508.361-508.361 0-280.765 227.623-508.572 508.361-508.572 280.765 0 508.572 227.808 508.572 508.572 0 280.739-227.808 508.361-508.572 508.361v0zM617.813 168.391c-26.27-10.297-47.076-18.072-62.626-23.537-15.552-5.464-33.625-8.196-54.22-8.196-31.759 0-56.506 7.775-74 23.328-17.626 15.342-26.244 35.095-26.244 58.843 0 9.247 0.631 18.703 1.893 28.37 1.287 9.667 3.389 20.385 6.278 32.573l32.811 115.795c2.917 11.14 5.439 21.646 7.33 31.313 1.944 10.087 2.969 19.123 2.969 27.319 0 14.712-3.126 25.009-9.273 30.867-6.068 5.884-17.627 8.616-34.86 8.616-8.379 0-17.206-1.233-26.083-3.755-8.827-2.759-16.366-5.281-22.645-7.593l8.59 35.516c21.435 8.801 42.030 16.182 61.547 22.487 19.597 6.331 38.038 9.457 55.533 9.457 31.497 0 55.664-7.75 72.687-22.906 17.022-15.131 25.638-34.884 25.638-59.236 0-5.045-0.63-13.87-1.682-26.48-1.26-12.82-3.361-24.378-6.514-34.885l-32.574-115.377c-2.731-9.247-5.045-19.964-7.356-31.732-2.101-11.768-3.152-20.805-3.152-26.898 0-15.342 3.572-25.85 10.297-31.313 6.936-5.674 18.704-8.406 35.727-8.406 7.987 0 16.813 1.473 26.899 4.203 9.878 2.732 17.233 5.254 21.856 7.355l-8.826-35.728zM612.138 636.587c-15.341-14.054-33.624-21.199-55.060-21.199-21.228 0-39.72 7.146-55.061 21.199-15.104 14.081-22.907 31.288-22.907 51.461 0 20.017 7.803 37.25 22.907 51.54 15.341 14.053 33.835 21.199 55.061 21.199 21.435 0 39.719-7.146 55.060-21.199 15.132-14.291 22.696-31.523 22.696-51.54 0-20.175-7.564-37.382-22.696-51.461v0z" />
<glyph unicode="&#xe618;" d="M755.931 195.563c-9.354-8.791-19.24-17.353-29.659-25.331-153.712-117.76-373.92-88.65-491.678 65.058-117.801 153.77-88.547 373.906 65.162 491.664 143.258 109.751 344.201 91.945 466.253-35.464l-122.046-93.502 375.131-105.272-3.973 389.62-124.908-95.692c-174.839 195.54-473.589 226.221-685.264 64.057-222.122-170.17-264.178-488.105-94.047-710.172 170.127-222.066 488.049-264.221 710.173-94.047 10.362 7.941 20.379 16.335 30.006 24.877l-95.15 124.204z" />
<glyph unicode="&#xe619;" d="M512.007 408.956c21.146 0 39.455 7.721 54.896 23.178 15.457 15.445 23.192 33.754 23.192 54.896v390.407c0 21.152-7.735 39.451-23.192 54.896-15.44 15.46-33.743 23.188-54.896 23.188s-39.452-7.736-54.903-23.188c-15.457-15.446-23.186-33.751-23.186-54.896v-390.407c0-21.142 7.729-39.451 23.186-54.895 15.451-15.458 33.758-23.179 54.903-23.179zM931.388 618.189c-32.741 65.468-78.783 120.371-138.16 164.694-17.080 13.011-36.391 18.095-57.953 15.252-21.554-2.848-38.628-13.016-51.239-30.497-13.016-17.077-17.987-36.288-14.945-57.642 3.042-21.36 13.117-38.536 30.197-51.551 39.839-30.091 70.667-66.897 92.404-110.412 21.758-43.509 32.629-89.875 32.629-139.071 0-42.305-8.237-82.658-24.697-121.088-16.474-38.43-38.737-71.675-66.8-99.737-28.063-28.055-61.293-50.327-99.73-66.792-38.431-16.475-78.798-24.713-121.089-24.713s-82.649 8.238-121.079 24.713c-38.438 16.465-71.676 38.735-99.739 66.792-28.056 28.062-50.318 61.308-66.792 99.737-16.473 38.431-24.705 78.784-24.705 121.088 0 49.196 10.879 95.557 32.632 139.071 21.758 43.507 52.563 80.314 92.412 110.412 17.080 13.021 27.14 30.197 30.196 51.551 3.048 21.342-1.93 40.565-14.945 57.642-12.604 17.481-29.575 27.648-50.933 30.497-21.35 2.843-40.777-2.242-58.258-15.252-59.377-44.323-105.426-99.226-138.168-164.694-32.736-65.475-49.106-135.217-49.106-209.226 0-63.442 12.408-124.026 37.208-181.781 24.816-57.746 58.157-107.563 100.045-149.449 41.881-41.882 91.701-75.237 149.449-100.030 57.748-24.808 118.339-37.224 181.781-37.224 63.437 0 124.036 12.416 181.782 37.224 57.747 24.801 107.568 58.149 149.449 100.030 41.882 41.887 75.223 91.703 100.037 149.449 24.802 57.755 37.204 118.339 37.204 181.781 0.004 74.009-16.346 143.751-49.087 209.226z" />
<glyph unicode="&#xe61a;" d="M886.804 444.014c-46.095 0-83.482 37.389-83.482 83.483 0 46.072 37.388 83.381 83.482 83.381s83.392-37.309 83.392-83.381c0.001-46.095-37.298-83.483-83.392-83.483v0zM886.804 235.443c-46.095 0-83.482 37.297-83.482 83.482 0 46.096 37.388 83.392 83.482 83.392s83.392-37.296 83.392-83.392c0.001-46.093-37.298-83.482-83.392-83.482v0zM764.924 110.264c0-46.095-40.596-89.989-86.781-89.989h-500.718c-46.002 0-79.268 43.895-79.268 89.989v417.233c0 46.072 33.265 90.528 79.268 90.528h500.716c46.094 0 86.782-44.456 86.782-90.528l0.001-417.233zM928.5 687.029h-309.282c-25.385 0-32.807 19.508-16.495 39.027l81.284 99.359c16.312 19.519 30.882 35.911 32.623 35.465 1.008-0.275 2.107 0 3.3 0 23.001 0 41.695 18.866 41.695 41.97 0 22.989-18.694 41.786-41.695 41.786-23.093 0-41.787-18.706-41.787-41.627 0-2.555 0.274-4.925 0.824-7.217 0.825-3.94-11.089-22.738-27.309-42.257l-103.277-123.792c-16.221-19.519-32.898-35.281-37.113-35.281-4.204 0-20.791 15.762-37.022 35.281l-103.175 123.793c-16.333 19.519-28.316 38.409-27.502 42.349 0.447 2.279 0.733 4.662 0.733 7.125 0 23.013-18.602 41.718-41.685 41.718-23.023 0-41.718-18.706-41.718-41.718 0-23.081 18.694-41.776 41.718-41.776 1.191 0 2.27 0.286 3.276 0.561 1.833 0.355 16.426-15.041 32.727-34.559l81.192-101.25c16.312-19.52 8.9-39.038-16.483-39.038h-309.296c-46.106 0.081-87.881-29.886-87.881-75.98v-584.186c0-46.095 41.775-75.513 87.881-75.513h834.466c46.094 0 89.349 29.418 89.349 75.513v584.187c0 46.095-43.252 76.060-89.348 76.060z" />
<glyph unicode="&#xe61b;" d="M978.173 521.246l-99.648 16.622c-7.242 29.613-17.806 57.915-31.56 84.316l63.793 79.093c13.566 16.719 12.953 40.77-1.35 56.901l-39.634 44.606c-14.368 16.119-38.187 19.521-56.359 8.042l-85.221-53.568c-37.479 26.187-79.942 45.525-125.743 56.747l-16.67 100.182c-3.498 21.215-21.857 36.778-43.405 36.778h-59.726c-21.506 0-39.924-15.563-43.345-36.778l-16.757-100.214c-37.87-9.271-73.375-24.236-105.773-43.948l-81.044 57.838c-17.47 12.478-41.459 10.525-56.704-4.682l-42.195-42.227c-15.208-15.239-17.157-39.233-4.648-56.698l57.976-81.196c-19.527-32.157-34.339-67.46-43.608-104.989l-100.754-16.824c-21.183-3.498-36.778-21.855-36.778-43.406v-59.68c0-21.549 15.595-39.907 36.778-43.405l100.752-16.824c7.614-30.879 18.666-60.382 33.352-87.732l-63.497-78.647c-13.524-16.704-12.954-40.768 1.348-56.886l39.602-44.604c14.369-16.15 38.204-19.494 56.376-8.041l86.449 54.303c36.574-25.021 77.956-43.406 122.366-54.303l16.756-100.205c3.422-21.215 21.841-36.777 43.346-36.777h59.726c21.548 0 39.907 15.563 43.403 36.777l16.704 100.205c37.392 9.142 72.442 23.852 104.556 43.217l84.421-60.287c17.437-12.528 41.448-10.564 56.671 4.695l42.211 42.212c15.196 15.194 17.252 39.173 4.632 56.672l-60.102 84.267c19.58 32.262 34.47 67.625 43.685 105.324l99.643 16.639c21.249 3.503 36.778 21.861 36.778 43.41v59.676c-0.031 21.544-15.56 39.902-36.803 43.399v0zM513.579 259.388c-104.145 0-188.609 84.481-188.609 188.615 0 104.145 84.465 188.61 188.609 188.61 104.129 0 188.607-84.465 188.607-188.61 0.002-104.134-84.478-188.615-188.607-188.615v0z" />
<glyph unicode="&#xe61c;" d="M952.822 703.538c-45.527 77.982-107.26 139.712-185.283 185.243-77.983 45.522-163.155 68.295-255.515 68.295-92.363 0-177.537-22.774-255.519-68.294-78.022-45.491-139.766-107.262-185.288-185.244-45.531-78.024-68.289-163.197-68.289-255.555 0-92.33 22.758-177.503 68.289-255.525 45.522-77.982 107.267-139.756 185.249-185.243 78.022-45.526 163.197-68.288 255.559-68.288 92.359 0 177.531 22.763 255.515 68.288 77.982 45.487 139.757 107.261 185.283 185.243 45.486 78.022 68.249 163.195 68.249 255.525-0.001 92.358-22.762 177.571-68.25 255.555v0zM842.106 418.819l-60.306-60.306c-8.382-8.382-18.354-12.592-29.834-12.592s-21.452 4.21-29.835 12.592l-125.256 125.263v-332.753c0-11.48-4.211-21.411-12.593-29.833-8.423-8.382-18.354-12.593-29.835-12.593h-84.849c-11.487 0-21.462 4.211-29.839 12.593-8.383 8.422-12.594 18.353-12.594 29.833v332.753l-125.263-125.26c-7.984-7.946-17.907-11.919-29.829-11.919-11.958 0-21.895 3.973-29.839 11.919l-60.333 60.305c-7.956 7.984-11.924 17.916-11.924 29.829 0 11.918 3.968 21.894 11.924 29.839l300.287 300.285c7.94 7.955 17.92 11.918 29.839 11.918 11.912 0 21.844-3.963 29.829-11.918l300.251-300.286c7.983-7.944 11.958-17.921 11.958-29.839-0.002-11.914-3.975-21.845-11.959-29.83v0z" />
<glyph unicode="&#xe61d;" d="M952.924 703.596c-45.539 78-107.285 139.744-185.326 185.286-78.002 45.531-163.193 68.31-255.574 68.31-92.384 0-177.576-22.78-255.577-68.31-78.040-45.502-139.798-107.286-185.33-185.286-45.542-78.042-68.305-163.234-68.305-255.613 0-92.351 22.763-177.544 68.305-255.583 45.532-78 107.29-139.788 185.291-185.286 78.041-45.537 163.233-68.305 255.617-68.305 92.38 0 177.572 22.769 255.574 68.305 78 45.498 139.787 107.286 185.326 185.286 45.495 78.039 68.264 163.232 68.264 255.583 0 92.379-22.769 177.612-68.265 255.613v0zM842.182 417.501l-300.321-300.36c-7.987-7.947-17.92-11.921-29.836-11.921-11.921 0-21.904 3.974-29.847 11.921l-300.354 300.36c-7.957 7.947-11.926 17.882-11.926 29.846 0 11.91 3.969 21.851 11.926 29.832l60.348 60.323c8.384 8.384 18.323 12.591 29.846 12.591 11.484 0 21.412-4.207 29.836-12.591l125.29-125.286v332.819c0 11.487 4.212 21.426 12.597 29.806 8.379 8.424 18.357 12.637 29.846 12.637h84.868c11.484 0 21.418-4.212 29.843-12.637 8.383-8.379 12.595-18.318 12.595-29.806v-332.819l125.286 125.286c7.947 7.947 17.881 11.921 29.841 11.921 11.921 0 21.855-3.974 29.842-11.921l60.318-60.323c7.988-7.981 11.962-17.922 11.962-29.832-0.001-11.964-3.973-21.899-11.96-29.846v0z" />
<glyph unicode="&#xe61e;" d="M225.052 951.414h270.202l303.693-503.477-308.728-503.352h-265.167l305.44 503.415z" />
<glyph unicode="&#xe61f;" d="M321.004 749.518l2.59-368.039c0 0-1.107-34.075 28.826-6.813 27.896 25.408 347.096 351.954 347.096 351.954s27.318 23.86-2.313 52.649c-29.632 28.787-152.717 153.75-152.717 153.75s-26.11 39.5-59.318 4.99c-33.21-34.507-148.268-147.226-148.268-147.226s-16.092-13.191-15.896-41.265zM194.994 614.291c0 0 1.098 33.375-24.556 8.869-25.653-24.507-157.259-157.427-157.259-157.427s-21.063-17.738 2.218-39.911c23.281-22.172 157.426-159.468 157.426-159.468s20.529-15.024 20.164 7.791c-0.366 22.818 2.007 340.146 2.007 340.146zM707.652 166.796c11.927-11.929 11.797-31.401-0.291-43.489l-170.306-170.307c-12.091-12.090-31.558-12.218-43.487-0.289l-171.051 171.051c-11.928 11.926-11.798 31.399 0.292 43.489l170.305 170.303c12.090 12.090 31.562 12.221 43.489 0.291l171.049-171.049zM1010.698 465.219c11.926-11.928 11.796-31.401-0.293-43.488l-170.306-170.308c-12.092-12.090-31.558-12.218-43.489-0.29l-171.048 171.052c-11.929 11.926-11.799 31.399 0.29 43.489l170.304 170.305c12.093 12.088 31.563 12.22 43.49 0.29l171.052-171.050z" />
<glyph unicode="&#xe620;" d="M729.285 747.346c-9.132-30.831-28.073-51.848-37.205-82.679-132.709 71.811-295.755 38.606-380.324-41.339-120.632-114.044-110.831-335.021 0-458.869 104.56-116.85 295.774-154.495 525.012-140.554-38.264-16.616-85.433-33.193-140.556-37.206-82.799-6.027-196.406-14.449-276.974 0-39.848 7.146-91.010 43.376-119.884 66.144-167.865 132.35-233.101 424.197-74.409 611.825 100.933 119.34 325.516 182.221 504.34 82.678zM526.722 916.838c-1.84-32.609-8.412-60.484-8.267-95.081-248.806 3.894-421.217-176.218-421.663-421.663-0.358-197.764 115.293-316.215 235.636-409.261-183.138-3.013-286.509 139.053-318.316 305.912-36.184 189.848 44.123 381.295 157.092 487.807 82.29 77.591 202.286 138.542 355.518 132.286zM1018.661 164.459c-23.41-31.934-62.158-88.67-107.481-103.35-72.378-23.441-259.309-5.489-326.582 8.268-171.37 35.052-339.531 130.938-330.718 338.985 6.652 157.072 126.784 274.099 293.511 268.708 114.164-3.691 186.047-59.381 239.768-148.822-26.79-14.546-53.652-29.025-78.545-45.473-95.176 170.786-386.294 111.183-359.652-115.754 4.832-41.173 24.663-73.938 45.473-99.213 113.596-137.978 395.536-131.411 624.226-103.349z" />
<glyph unicode="&#xe621;" d="M4.283 787.656c7.274-57.855 34.374-98.399 78.67-119.196 38.761-18.203 94.99-7.272 147.8-11.918 121.794-10.718 187.114-118.102 185.942-214.548-1.327-109.177-85.133-194.234-185.942-202.633-34.385-2.865-67.842 5.006-107.273 0-70.178-8.906-108.898-56.879-119.195-138.263 87.799 4.418 170.766-5.942 247.926 0 125.395 9.656 202.723 87.278 259.842 164.488 57.558-77.95 135.042-155.379 262.226-164.488 76.165-5.456 158.659 4.056 245.54 0-5.646 62.881-31.961 102.074-73.9 123.963-42.755 22.31-99.255 8.868-154.953 14.301-242.313 23.646-239.659 396.828 4.766 417.183 36.153 3.011 70.608-4.252 104.892 0 72.445 8.981 109.527 59.474 119.194 138.264-87.783-4.638-170.775 6.243-247.925 0-125.192-10.134-202.138-87.567-259.843-164.49-56.746 78.254-140.256 155.752-262.225 164.49-71.555 5.126-152.863-3.755-243.154 0-2.777-0.399-2.588-3.768-2.385-7.153h-0.003z" />
<glyph unicode="&#xe622;" d="M141.308 212.259c-74.194 0-134.474-60.515-134.474-134.262 0-74.15 60.285-133.986 134.472-133.986 74.459 0 134.675 59.84 134.675 133.986-0.001 73.747-60.21 134.262-134.673 134.262zM6.988 609.88v-193.637c126.078 0 244.648-49.305 333.939-138.637 89.178-89.113 138.41-208.223 138.41-334.772h194.488c0 367.842-299.23 667.046-666.837 667.046zM7.217 953.166v-193.735c449.698 0 815.72-366.379 815.72-816.597h194.229c0 556.934-453.123 1010.332-1009.949 1010.332z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Binary file not shown.

View file

@ -6197,4 +6197,3 @@ button.close {
display: none !important;
}
}
/*# sourceMappingURL=bootstrap.css.map */

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,83 @@
#import sickbeard
#import os.path
#set global $title='Cache'
#set global $header='Cache'
#set global $sbPath='..'
#set global $topmenu='cache'#
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
<style type="text/css">
.sort_data {display:none}
</style>
<script type="text/javascript">
<!--
\$(document).ready(function()
{
\$('#cacheTable:has(tbody tr)').tablesorter({
widgets: ['zebra', 'filter'],
sortList: [[0,1]],
});
#raw
$('.addQTip').each(function () {
$(this).css({'cursor':'help', 'text-shadow':'0px 0px 0.5px #666'});
$(this).qtip({
show: {solo:true},
position: {viewport:$(window), my:'right center', adjust:{ y: -10, x: -15 }},
style: {classes:'qtip-rounded qtip-shadow'}
});
});
#end raw
});
//-->
</script>
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#end if
<table id="cacheTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead>
<tr>
<th class="col-cache">Provider</th>
<th class="col-name-cache">Name</th>
<th class="col-cache">Season</th>
<th class="col-episodes">Episodes</th>
<th class="col-cache">Indexer Id</th>
<th class="col-cache">Url</th>
<th class="col-cache">Time</th>
<th class="col-cache">Quality</th>
<th class="col-cache">Release Group</th>
<th class="col-cache">Version</th>
</tr>
</thead>
<tfoot>
<tr>
<th class="nowrap" colspan="10">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $cacheResults:
<tr>
<td class="col-cache">$hItem['provider']</td>
<td class="col-name-cache">$hItem['name']</td>
<td class="col-cache">$hItem['season']</td>
<td class="col-episodes">$hItem['episodes']</td>
<td class="col-cache">$hItem['indexerid']</td>
<td class="col-cache"><span title="$hItem['url']" class="addQTip"><img src="$sbRoot/images/info32.png" width="16" height="16" /></span></td>
<td class="col-cache">$hItem['time']</td>
<td class="col-cache">$hItem['quality']</td>
<td class="col-cache">$hItem['release_group']</td>
<td class="col-cache">$hItem['version']</td>
</tr>
#end for
</tbody>
</table>
#include $os.path.join($sickbeard.PROG_DIR,'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -94,7 +94,7 @@
<label for="anidb_password">
<span class="component-title">AniDB password</span>
<span class="component-desc">
<input type="password" name="anidb_password" id="anidb_password" value="$sickbeard.ANIDB_PASSWORD" class="form-control input-sm input350" />
<input type="password" name="anidb_password" id="anidb_password" value="#echo '*' * len($sickbeard.ANIDB_PASSWORD)#" class="form-control input-sm input350" />
</span>
</label>
</div>

View file

@ -8,6 +8,8 @@
#from sickbeard import metadata
#from sickbeard.metadata.generic import GenericMetadata
#from sickbeard.helpers import anon_url
#from sickbeard.logger import reverseNames as file_logging_presets
#from sickbeard.helpers import maybe_plural
#set global $title = 'Config - General'
#set global $header = 'General Configuration'
@ -22,6 +24,8 @@
#else
<h1 class="title">$title</h1>
#end if
#set $checked = ' checked="checked"'
#set $selected = ' selected="selected"'
#set $indexer = 0
#if $sickbeard.INDEXER_DEFAULT
@ -57,7 +61,7 @@
<label for="launch_browser">
<span class="component-title">Launch browser</span>
<span class="component-desc">
<input type="checkbox" name="launch_browser" id="launch_browser" #if $sickbeard.LAUNCH_BROWSER then 'checked="checked"' else ''#>
<input type="checkbox" name="launch_browser" id="launch_browser"#echo ('', $checked)[$sickbeard.LAUNCH_BROWSER]#>
<p>open the SickGear home page on startup</p>
</span>
</label>
@ -67,7 +71,7 @@
<label for="update_shows_on_start">
<span class="component-title">Update shows on startup</span>
<span class="component-desc">
<input type="checkbox" name="update_shows_on_start" id="update_shows_on_start" #if $sickbeard.UPDATE_SHOWS_ON_START then 'checked="checked"' else ''#>
<input type="checkbox" name="update_shows_on_start" id="update_shows_on_start"#echo ('', $checked)[$sickbeard.UPDATE_SHOWS_ON_START]#>
<p>with show data; episode plot, images, air and end dates, etc. Disable for a quicker startup. Show data is scheduled to update during hour <span class="show_update_hour_value">$sickbeard.SHOW_UPDATE_HOUR</span>.</p>
</span>
</label>
@ -87,11 +91,11 @@
<span class="component-title">Send to trash for actions</span>
<span class="component-desc">
<label for="trash_remove_show" class="nextline-block">
<input type="checkbox" name="trash_remove_show" id="trash_remove_show" #if $sickbeard.TRASH_REMOVE_SHOW then 'checked="checked"' else ''#>
<input type="checkbox" name="trash_remove_show" id="trash_remove_show"#echo ('', $checked)[$sickbeard.TRASH_REMOVE_SHOW]#>
<p>when using show "Remove" and delete files</p>
</label>
<label for="trash_rotate_logs" class="nextline-block">
<input type="checkbox" name="trash_rotate_logs" id="trash_rotate_logs" #if $sickbeard.TRASH_ROTATE_LOGS then 'checked="checked"' else ''#>
<input type="checkbox" name="trash_rotate_logs" id="trash_rotate_logs"#echo ('', $checked)[$sickbeard.TRASH_ROTATE_LOGS]#>
<p>on scheduled deletes of the oldest log files</p>
</label>
<div class="clear-left"><p>selected actions use trash (recycle bin) instead of the default permanent delete</p></div>
@ -112,9 +116,9 @@
<span class="component-title">Use initial indexer set to</span>
<span class="component-desc">
<select id="indexer_default" name="indexer_default" class="form-control input-sm">
<option value="0" #if $indexer == 0 then 'selected="selected"' else ''#>All Indexers</option>
<option value="0"#echo ('', $selected)[0 == $indexer]#>All Indexers</option>
#for $indexer in $sickbeard.indexerApi().indexers
<option value="$indexer" #if $indexer == $sickbeard.INDEXER_DEFAULT then 'selected="selected"' else ''#>$sickbeard.indexerApi().indexers[$indexer]</option>
<option value="$indexer"#echo ('', $selected)[$indexer == $sickbeard.INDEXER_DEFAULT]#>$sickbeard.indexerApi().indexers[$indexer]</option>
#end for
</select>
<span>as the default selection when adding new shows</span>
@ -157,7 +161,7 @@
<label for="version_notify">
<span class="component-title">Check software updates</span>
<span class="component-desc">
<input type="checkbox" name="version_notify" id="version_notify" #if $sickbeard.VERSION_NOTIFY then 'checked="checked"' else ''#>
<input type="checkbox" name="version_notify" id="version_notify"#echo ('', $checked)[$sickbeard.VERSION_NOTIFY]#>
<p>and display notifications when updates are available.
Checks are run on startup and at the frequency set below*</p>
</span>
@ -168,7 +172,7 @@
<label for="auto_update">
<span class="component-title">Automatically update</span>
<span class="component-desc">
<input type="checkbox" name="auto_update" id="auto_update" #if $sickbeard.AUTO_UPDATE then 'checked="checked"' else ''#>
<input type="checkbox" name="auto_update" id="auto_update"#echo ('', $checked)[$sickbeard.AUTO_UPDATE]#>
<p>fetch and install software updates.
Updates are run on startup and in the background at the frequency set below*</p>
</span>
@ -189,7 +193,7 @@
<label for="notify_on_update">
<span class="component-title">Notify on software update</span>
<span class="component-desc">
<input type="checkbox" name="notify_on_update" id="notify_on_update" #if $sickbeard.NOTIFY_ON_UPDATE then 'checked="checked"' else ''#>
<input type="checkbox" name="notify_on_update" id="notify_on_update"#echo ('', $checked)[$sickbeard.NOTIFY_ON_UPDATE]#>
<p>send a message to all enabled notifiers when SickGear has been updated</p>
</span>
</label>
@ -217,19 +221,42 @@
<span class="component-title">Display theme:</span>
<span class="component-desc">
<select id="theme_name" name="theme_name" class="form-control input-sm">
<option value="dark" #if $sickbeard.THEME_NAME == 'dark' then 'selected="selected"' else ''#>Dark</option>
<option value="light" #if $sickbeard.THEME_NAME == 'light' then 'selected="selected"' else ''#>Light</option>
<option value="dark"#echo ('', $selected)['dark' == $sickbeard.THEME_NAME]#>Dark</option>
<option value="light"#echo ('', $selected)['light' == $sickbeard.THEME_NAME]#>Light</option>
</select>
<span class="red-text">for appearance to take effect, save then refresh your browser</span>
</span>
</label>
</div>
<div class="field-pair">
<label for="default_home">
<span class="component-title">Use as default home page:</span>
<span class="component-desc">
<select id="default_home" name="default_home" class="form-control input-sm">
<option value="shows"#echo ('', $selected)['shows' == $sickbeard.DEFAULT_HOME]#>Shows</option>
<option value="episodes"#echo ('', $selected)['episodes' == $sickbeard.DEFAULT_HOME]#>Episodes</option>
<option value="history"#echo ('', $selected)['history' == $sickbeard.DEFAULT_HOME]#>History</option>
</select>
</span>
</label>
</div>
<div class="field-pair">
<label for="use_imdb_info">
<span class="component-title">Enable IMDb info</span>
<span class="component-desc">
<input type="checkbox" name="use_imdb_info" id="use_imdb_info"#echo ('', $checked)[$sickbeard.USE_IMDB_INFO]#>
<p>for ui links, display show; ratings, country flag, year, runtime, and genre tags</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="home_search_focus">
<span class="component-title">Give show list search focus</span>
<span class="component-desc">
<input type="checkbox" name="home_search_focus" id="home_search_focus" #if $sickbeard.HOME_SEARCH_FOCUS then 'checked="checked"' else ''#>
<input type="checkbox" name="home_search_focus" id="home_search_focus"#echo ('', $checked)[$sickbeard.HOME_SEARCH_FOCUS]#>
<p>page refresh on "Show List" will start search box focused</p>
</span>
</label>
@ -239,7 +266,7 @@
<label for="sort_article">
<span class="component-title">Sort with "The", "A", "An"</span>
<span class="component-desc">
<input type="checkbox" name="sort_article" id="sort_article" #if $sickbeard.SORT_ARTICLE then 'checked="checked"' else ''#>
<input type="checkbox" name="sort_article" id="sort_article"#echo ('', $checked)[$sickbeard.SORT_ARTICLE]#>
<p>include articles ("The", "A", "An") when sorting show lists</p>
</span>
</label>
@ -249,16 +276,16 @@
<label for="fuzzy_dating">
<span class="component-title">Display fuzzy dates</span>
<span class="component-desc">
<input type="checkbox" name="fuzzy_dating" id="fuzzy_dating" class="viewIf datePresets" #if $sickbeard.FUZZY_DATING == True then 'checked="checked"' else ''#>
<input type="checkbox" name="fuzzy_dating" id="fuzzy_dating" class="viewIf datePresets"#echo ('', $checked)[$sickbeard.FUZZY_DATING == True]#>
<p>move absolute dates into tooltips and display e.g. "Last Thu", "On Tue"</p>
</span>
</label>
</div>
<div class="field-pair show_if_fuzzy_dating#if True == $sickbeard.FUZZY_DATING then '' else ' metadataDiv'#">
<div class="field-pair show_if_fuzzy_dating#echo (' metadataDiv', '')[$sickbeard.FUZZY_DATING]#">
<label for="trim_zero">
<span class="component-title">Trim date and time</span>
<span class="component-desc">
<input type="checkbox" name="trim_zero" id="trim_zero" #if True == $sickbeard.TRIM_ZERO then 'checked="checked"' else ''#>
<input type="checkbox" name="trim_zero" id="trim_zero"#echo ('', $checked)[True == $sickbeard.TRIM_ZERO]#>
<p>display minimalist date and time i.e. <del>02:00</del> = 2:00, <del>02:00pm</del> = 2pm, <del>03 Jan</del> = 3 Jan</p>
</span>
</label>
@ -268,16 +295,16 @@
<label for="date_presets">
<span class="component-title">Date style:</span>
<span class="component-desc">
<select class="form-control input-sm #if True == $sickbeard.FUZZY_DATING then '' else ' metadataDiv'#" id="date_presets#if True == $sickbeard.FUZZY_DATING then '' else '_na'#" name="date_preset#if True == $sickbeard.FUZZY_DATING then '' else '_na'#">
#for $cur_preset in $date_presets:
<option value="$cur_preset" #if $cur_preset == $sickbeard.DATE_PRESET or ("%x" == $sickbeard.DATE_PRESET and "$cur_preset" == '%a, %b %d, %Y') then 'selected="selected"' else ''#>$datetime.datetime($datetime.datetime.now().year, 12, 31, 14, 30, 47).strftime($cur_preset)</option>
<select class="form-control input-sm#echo (' metadataDiv', '')[$sickbeard.FUZZY_DATING]#" id="date_presets#echo ('_na', '')[$sickbeard.FUZZY_DATING]#" name="date_preset#echo ('_na', '')[$sickbeard.FUZZY_DATING]#">
#for $cur_preset in $date_presets
<option value="$cur_preset" #echo ('', $selected)[$cur_preset == $sickbeard.DATE_PRESET or ('%x' == $sickbeard.DATE_PRESET and '$cur_preset' == '%a, %b %d, %Y')]#>$datetime.datetime($datetime.datetime.now().year, 12, 31, 14, 30, 47).strftime($cur_preset)</option>
#end for
</select>
<select class="form-control input-sm #if True != $sickbeard.FUZZY_DATING then '' else ' metadataDiv'#" id="date_presets#if True != $sickbeard.FUZZY_DATING then '' else '_na'#" name="date_preset#if True != $sickbeard.FUZZY_DATING then '' else '_na'#">
<option value="%x" #if "%x" == $sickbeard.DATE_PRESET then 'selected="selected"' else ''#>Use System Default</option>
#for $cur_preset in $date_presets:
<option value="$cur_preset" #if $cur_preset == $sickbeard.DATE_PRESET then 'selected="selected"' else ''#>$datetime.datetime($datetime.datetime.now().year, 12, 31, 14, 30, 47).strftime($cur_preset)</option>
<select class="form-control input-sm#echo ('', ' metadataDiv')[$sickbeard.FUZZY_DATING]#" id="date_presets#echo (', ''_na')[$sickbeard.FUZZY_DATING]#" name="date_preset#echo ('', '_na')[$sickbeard.FUZZY_DATING]#">
<option value="%x"#echo ('', $selected)['%x' == $sickbeard.DATE_PRESET]#>Use System Default</option>
#for $cur_preset in $date_presets
<option value="$cur_preset"#echo ('', $selected)[$cur_preset == $sickbeard.DATE_PRESET]#>$datetime.datetime($datetime.datetime.now().year, 12, 31, 14, 30, 47).strftime($cur_preset)</option>
#end for
</select>
</span>
@ -289,9 +316,9 @@
<span class="component-title">Time style:</span>
<span class="component-desc">
<select id="time_presets" name="time_preset" class="form-control input-sm">
#for $cur_preset in $time_presets:
#for $cur_preset in $time_presets
#set $show_seconds = not $sickbeard.FUZZY_DATING
<option value="$cur_preset" #if $cur_preset == $sickbeard.TIME_PRESET_W_SECONDS then 'selected="selected"' else ''#>$sbdatetime.now().sbftime(show_seconds=$show_seconds, t_preset=$cur_preset)</option>
<option value="$cur_preset"#echo ('', $selected)[$cur_preset == $sickbeard.TIME_PRESET_W_SECONDS]#>$sbdatetime.now().sbftime(show_seconds=$show_seconds, t_preset=$cur_preset)</option>
#end for
</select>
<span id="trim_info_seconds"><b>note:</b> seconds are only shown on the History page</span>
@ -303,10 +330,10 @@
<span class="component-title">Timezone:</span>
<span class="component-desc">
<label for="local" class="space-right">
<input type="radio" name="timezone_display" id="local" value="local" #if "local" == $sickbeard.TIMEZONE_DISPLAY then 'checked="checked"' else ''#>local
<input type="radio" name="timezone_display" id="local" value="local"#echo ('', $checked)['local' == $sickbeard.TIMEZONE_DISPLAY]#>local
</label>
<label for="network">
<input type="radio" name="timezone_display" id="network" value="network" #if "network" == $sickbeard.TIMEZONE_DISPLAY then 'checked="checked"' else ''#>network
<input type="radio" name="timezone_display" id="network" value="network"#echo ('', $checked)['network' == $sickbeard.TIMEZONE_DISPLAY]#>network
</label>
<div class="clear-left"><p>display dates and times in either your timezone or the shows network timezone</p></div>
</span>
@ -323,7 +350,7 @@
<div class="component-group-desc">
<h3>Web Interface</h3>
<p>It is recommended that you enable a username and password to secure SickGear from being tampered with remotely.</p>
<p><b class="boldest">These options require a manual restart to take effect.</b></p>
<p><b>These options require a manual restart to take effect.</b></p>
</div>
<fieldset class="component-group-list">
@ -342,9 +369,10 @@
<label for="web_password">
<span class="component-title">Password</span>
<span class="component-desc">
<input type="password" name="web_password" id="web_password" value="$sickbeard.WEB_PASSWORD" class="form-control input-sm input300">
<input type="password" name="web_password" id="web_password" value="#echo '*' * len($sickbeard.WEB_PASSWORD)#" class="form-control input-sm input300">
<p>blank for none</p>
<span class="clear-left">check autoProcessTV.cfg is set up for external apps to use post processing scripts
<span class="clear-left">check autoProcessTV.cfg is set up for external apps to use post processing scripts</span>
</span>
</label>
</div>
@ -352,7 +380,7 @@
<label for="calendar_unprotected">
<span class="component-title">Unprotected calendar</span>
<span class="component-desc">
<input type="checkbox" name="calendar_unprotected" id="calendar_unprotected" #if $sickbeard.CALENDAR_UNPROTECTED then 'checked="checked"' else ''#>
<input type="checkbox" name="calendar_unprotected" id="calendar_unprotected"#echo ('', $checked)[$sickbeard.CALENDAR_UNPROTECTED]#>
<p>permit subscribing to the calendar without username and password.
Some services like Google Calendar will only work with <b class="boldest">no</b> authentication</p>
</span>
@ -364,7 +392,7 @@
<label for="use_api">
<span class="component-title">API enable</span>
<span class="component-desc">
<input type="checkbox" name="use_api" class="enabler" id="use_api" #if $sickbeard.USE_API then 'checked="checked"' else ''#>
<input type="checkbox" name="use_api" class="enabler" id="use_api"#echo ('', $checked)[$sickbeard.USE_API]#>
<p>permit the use of the SickGear (SickBeard) API</p>
</span>
</label>
@ -396,7 +424,7 @@
<label for="web_log">
<span class="component-title">HTTP logs</span>
<span class="component-desc">
<input type="checkbox" name="web_log" id="web_log" #if $sickbeard.WEB_LOG then 'checked="checked"' else ''#>
<input type="checkbox" name="web_log" id="web_log"#echo ('', $checked)[$sickbeard.WEB_LOG]#>
<p>enable logs from the internal web server</p>
</span>
</label>
@ -406,7 +434,7 @@
<label for="enable_https">
<span class="component-title">SSL enable</span>
<span class="component-desc">
<input type="checkbox" name="enable_https" class="enabler" id="enable_https" #if $sickbeard.ENABLE_HTTPS then 'checked="checked"' else ''#>
<input type="checkbox" name="enable_https" class="enabler" id="enable_https"#echo ('', $checked)[$sickbeard.ENABLE_HTTPS]#>
<p>use a HTTPS address to access the web interface</p>
</span>
</label>
@ -436,7 +464,7 @@
<label for="web_ipv6">
<span class="component-title">Listen on IPv6</span>
<span class="component-desc">
<input type="checkbox" name="web_ipv6" id="web_ipv6" #if $sickbeard.WEB_IPV6 then 'checked="checked"' else ''#>
<input type="checkbox" name="web_ipv6" id="web_ipv6"#echo ('', $checked)[$sickbeard.WEB_IPV6]#>
<p>attempt binding to any available IPv6 address</p>
</span>
</label>
@ -446,7 +474,7 @@
<label for="handle_reverse_proxy">
<span class="component-title">Reverse proxy headers</span>
<span class="component-desc">
<input type="checkbox" name="handle_reverse_proxy" id="handle_reverse_proxy" #if $sickbeard.HANDLE_REVERSE_PROXY then 'checked="checked"' else ''#>
<input type="checkbox" name="handle_reverse_proxy" id="handle_reverse_proxy"#echo ('', $checked)[$sickbeard.HANDLE_REVERSE_PROXY]#>
<p>accept the following reverse proxy headers (advanced)...<br />(X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto)</p>
</span>
</label>
@ -474,33 +502,28 @@
<span class="component-title">Branch version:</span>
<span class="component-desc">
<select id="branchVersion" class="form-control form-control-inline input-sm pull-left max300">
#for $cur_branch in $sickbeard.versionCheckScheduler.action.list_remote_branches():
<option value="$cur_branch" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option>
#end for
<option>Loading list from github</option>
</select>
<input class="btn btn-inline" style="margin-left: 6px;" type="button" id="branchCheckout" value="Checkout Branch">
<input class="btn btn-inline" style="margin-left: 6px;" type="button" id="branchCheckout" value="Checkout Branch" disabled="disabled">
<div class="clear-left"><p>select branch to use (restart required)</p></div>
</span>
</label>
</div>
#set pulls = sickbeard.versionCheckScheduler.action.list_remote_pulls()
#if len(pulls) > 0 and $sickbeard.BRANCH != 'master':
#if $sickbeard.BRANCH != 'master'
<div class="field-pair">
<label>
<span class="component-title">Pull request:</span>
<span class="component-desc">
<select id="pullRequestVersion" class="form-control form-control-inline input-sm pull-left max300">
#for $cur_branch in $pulls:
<option value="$cur_branch.fetch_name()" #if $cur_branch == $sickbeard.BRANCH then 'selected="selected"' else ''#>$cur_branch</option>
#end for
<option>Loading list from github</option>
</select>
<input class="btn btn-inline" style="margin-left: 6px;" type="button" id="pullRequestCheckout" value="Checkout Pull Request">
<input class="btn btn-inline" style="margin-left: 6px;" type="button" id="pullRequestCheckout" value="Checkout Pull Request" disabled="disabled">
<div class="clear-left"><p>select pull request to test (restart required)</p></div>
</span>
</label>
</div>
#end if
#end if
<div class="field-pair">
<label for="git_remote">
@ -527,8 +550,8 @@
<span class="component-title">CPU throttling:</span>
<span class="component-desc">
<select id="cpu_presets" name="cpu_preset" class="form-control input-sm">
#for $cur_preset in $cpu_presets:
<option value="$cur_preset" #if $cur_preset == $sickbeard.CPU_PRESET then 'selected="selected"' else ''#>$cur_preset.capitalize()</option>
#for $cur_preset in $cpu_presets
<option value="$cur_preset"#echo ('', $selected)[$cur_preset == $sickbeard.CPU_PRESET]#>$cur_preset.capitalize()</option>
#end for
</select>
<span>Normal (default). High is lower and Low is higher CPU use</span>
@ -550,7 +573,7 @@
<label for="encryption_version">
<span class="component-title">Encrypt passwords</span>
<span class="component-desc">
<input type="checkbox" name="encryption_version" id="encryption_version" #if $sickbeard.ENCRYPTION_VERSION then 'checked="checked"' else ''#>
<input type="checkbox" name="encryption_version" id="encryption_version"#echo ('', $checked)[$sickbeard.ENCRYPTION_VERSION]#>
<p>in the <code>config.ini</code> file.
<b>Warning:</b> Passwords must only contain <a target="_blank" href="<%= anon_url('http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters') %>">ASCII characters</a></p>
</span>
@ -564,6 +587,7 @@
<input type="text" name="proxy_setting" value="$sickbeard.PROXY_SETTING" class="form-control input-sm input300">
<p>blank to disable</p>
<div class="clear-left"><p>proxy address for connecting to providers (use 'PAC:Url' for PAC support)</p></div>
</span>
</label>
</div>
@ -571,18 +595,39 @@
<label for="proxy_indexers">
<span class="component-title">Use proxy for indexers</span>
<span class="component-desc">
<input type="checkbox" name="proxy_indexers" id="proxy_indexers" #if True == $sickbeard.PROXY_INDEXERS then 'checked="checked"' else ''#>
<input type="checkbox" name="proxy_indexers" id="proxy_indexers"#echo ('', $checked)[True == $sickbeard.PROXY_INDEXERS]#>
<p>use proxy host for connecting to indexers (thetvdb, tvrage)</p>
</span>
</label>
</div>
<div class="field-pair">
<label>
<span class="component-title">File logging level:</span>
<span class="component-desc">
<select id="file_logging_presets" name="file_logging_preset" class="form-control input-sm">
#set $levels = $file_logging_presets.keys()
#set void = $levels.sort(lambda x, y: cmp($file_logging_presets[$x], $file_logging_presets[$y]))
#set $level_count = len($levels)
#for $level in $levels
#set $level_title = $level.title().upper()
#set $level_count -= 1
#set $level_text = '%s%s' % ($level.title(), (('', ' only')[0 == $level_count], ' and the next%s level%s' % ((' ' + str($level_count), '')[1 == $level_count], maybe_plural($level_count)))[0 < $level_count])
<option value="$level_title"#echo ('', $selected)[$level_title == $sickbeard.FILE_LOGGING_PRESET]#>$level_text</option>
#end for
</select>
<span>(default: Db)</span>
<p>enable Db or Debug to pin down an issue, the others are normal use</p>
</span>
</label>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes">
</fieldset>
</div><!-- /component-group3 //-->
<br/>
<h6 class="pull-right"><b class="boldest">All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b></h6>
<h6 class="pull-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b></h6>
<input type="submit" class="btn pull-left config_submitter button" value="Save Changes">
</div><!-- /config-components -->
@ -595,8 +640,8 @@
<script type="text/javascript" charset="utf-8">
<!--
jQuery('#log_dir').fileBrowser({ title: 'Select log file folder location' });
jQuery('#config-components').tabs();
jQuery('#log_dir').fileBrowser({ title: 'Select log file folder location' });
jQuery('#config-components').tabs();
//-->
</script>

View file

@ -1,6 +1,6 @@
#import sickbeard
#import re
#from sickbeard.helpers import anon_url
#from sickbeard.helpers import anon_url, starify
#set global $title = 'Config - Notifications'
#set global $header = 'Notifications'
@ -139,7 +139,7 @@
<div class="field-pair">
<label for="xbmc_password">
<span class="component-title">XBMC password</span>
<input type="password" name="xbmc_password" id="xbmc_password" value="$sickbeard.XBMC_PASSWORD" class="form-control input-sm input250" />
<input type="password" name="xbmc_password" id="xbmc_password" value="#echo '*' * len($sickbeard.XBMC_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -155,7 +155,127 @@
</div><!-- /xbmc component-group //-->
<div class="component-group">
<div class="component-group-desc">
<img class="notifier-icon" src="$sbRoot/images/notifiers/kodi.png" alt="" title="Kodi" />
<h3><a href="<%= anon_url('http://kodi.tv/') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">Kodi</a></h3>
<p>Kodi (formerly known as XBMC) is an award-winning free and open source (GPL) software media player and entertainment hub.</p>
</div>
<fieldset class="component-group-list">
<div class="field-pair">
<label class="cleafix" for="use_kodi">
<span class="component-title">Enable</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="use_kodi" id="use_kodi" #if $sickbeard.USE_KODI then "checked=\"checked\"" else ""# />
<p>should SickGear send Kodi commands ?<p>
</span>
</label>
</div>
<div id="content_use_kodi">
<div class="field-pair">
<label for="kodi_always_on">
<span class="component-title">Always on</span>
<span class="component-desc">
<input type="checkbox" name="kodi_always_on" id="kodi_always_on" #if $sickbeard.KODI_ALWAYS_ON then "checked=\"checked\"" else ""# />
<p>log errors when unreachable ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_notify_onsnatch">
<span class="component-title">Notify on snatch</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_onsnatch" id="kodi_notify_onsnatch" #if $sickbeard.KODI_NOTIFY_ONSNATCH then "checked=\"checked\"" else ""# />
<p>send a notification when a download starts ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_notify_ondownload">
<span class="component-title">Notify on download</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_ondownload" id="kodi_notify_ondownload" #if $sickbeard.KODI_NOTIFY_ONDOWNLOAD then "checked=\"checked\"" else ""# />
<p>send a notification when a download finishes ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_notify_onsubtitledownload">
<span class="component-title">Notify on subtitle download</span>
<span class="component-desc">
<input type="checkbox" name="kodi_notify_onsubtitledownload" id="kodi_notify_onsubtitledownload" #if $sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD then "checked=\"checked\"" else ""# />
<p>send a notification when subtitles are downloaded ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_update_library">
<span class="component-title">Update library</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_library" id="kodi_update_library" #if $sickbeard.KODI_UPDATE_LIBRARY then "checked=\"checked\"" else ""# />
<p>update Kodi library when a download finishes ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_update_full">
<span class="component-title">Full library update</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_full" id="kodi_update_full" #if $sickbeard.KODI_UPDATE_FULL then "checked=\"checked\"" else ""# />
<p>perform a full library update if update per-show fails ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_update_onlyfirst">
<span class="component-title">Only update first host</span>
<span class="component-desc">
<input type="checkbox" name="kodi_update_onlyfirst" id="kodi_update_onlyfirst" #if $sickbeard.KODI_UPDATE_ONLYFIRST then "checked=\"checked\"" else ""# />
<p>only send library updates to the first active host ?</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_host">
<span class="component-title">Kodi IP:Port</span>
<input type="text" name="kodi_host" id="kodi_host" value="$sickbeard.KODI_HOST" class="form-control input-sm input350" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">host running Kodi (eg. 192.168.1.100:8080)</span>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">(multiple host strings must be separated by commas)</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_username">
<span class="component-title">Kodi username</span>
<input type="text" name="kodi_username" id="kodi_username" value="$sickbeard.KODI_USERNAME" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">username for your KODI server (blank for none)</span>
</label>
</div>
<div class="field-pair">
<label for="kodi_password">
<span class="component-title">Kodi password</span>
<input type="password" name="kodi_password" id="kodi_password" value="#echo '*' * len($sickbeard.KODI_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">password for your KODI server (blank for none)</span>
</label>
</div>
<div class="testNotification" id="testKODI-result">Click below to test.</div>
<input class="btn" type="button" value="Test Kodi" id="testKODI" />
<input type="submit" class="config_submitter btn" value="Save Changes" />
</div><!-- /content_use_kodi //-->
</fieldset>
</div><!-- /kodi component-group //-->
<div class="component-group">
<div class="component-group-desc">
<img class="notifier-icon" src="$sbRoot/images/notifiers/plex.png" alt="" title="Plex Media Server" />
@ -189,7 +309,7 @@
<label for="plex_password">
<span class="component-title">Server/client password</span>
<span class="component-desc">
<input type="password" name="plex_password" id="plex_password" value="$sickbeard.PLEX_PASSWORD" class="form-control input-sm input250" />
<input type="password" name="plex_password" id="plex_password" value="#echo '*' * len($sickbeard.PLEX_PASSWORD)#" class="form-control input-sm input250" />
<p>blank = no authentication</p>
</span>
</label>
@ -641,7 +761,7 @@
<div class="field-pair">
<label for="growl_password">
<span class="component-title">Growl password</span>
<input type="password" name="growl_password" id="growl_password" value="$sickbeard.GROWL_PASSWORD" class="form-control input-sm input250" />
<input type="password" name="growl_password" id="growl_password" value="#echo '*' * len($sickbeard.GROWL_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -709,7 +829,7 @@
<div class="field-pair">
<label for="prowl_api">
<span class="component-title">Prowl API key:</span>
<input type="text" name="prowl_api" id="prowl_api" value="$sickbeard.PROWL_API" class="form-control input-sm input250" />
<input type="text" name="prowl_api" id="prowl_api" value="<%= starify(sickbeard.PROWL_API) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -843,7 +963,7 @@
<div class="field-pair">
<label for="pushover_userkey">
<span class="component-title">Pushover key</span>
<input type="text" name="pushover_userkey" id="pushover_userkey" value="$sickbeard.PUSHOVER_USERKEY" class="form-control input-sm input250" />
<input type="text" name="pushover_userkey" id="pushover_userkey" value="<%= starify(sickbeard.PUSHOVER_USERKEY) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -853,15 +973,80 @@
<div class="field-pair">
<label for="pushover_apikey">
<span class="component-title">Pushover API key</span>
<input type="text" name="pushover_apikey" id="pushover_apikey" value="$sickbeard.PUSHOVER_APIKEY" class="form-control input-sm input250" />
<input type="text" name="pushover_apikey" id="pushover_apikey" value="<%= starify(sickbeard.PUSHOVER_APIKEY) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc"><a href="<%= anon_url('https://pushover.net/apps/clone/SickGear') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><b>Click here</b></a> to create a Pushover API key</span>
<span class="component-desc"><a href="<%= anon_url('https://pushover.net/apps/') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><b>Click here</b></a> to create a Pushover API key</span>
</label>
</div>
<div class="field-pair">
<label for="pushover_priority">
<span class="component-title">Pushover priority:</span>
<select id="pushover_priority" name="pushover_priority" class="form-control input-sm">
<option value="-2" #if $sickbeard.PUSHOVER_PRIORITY == -2 then 'selected="selected"' else ""#>Lowest</option>
<option value="-1" #if $sickbeard.PUSHOVER_PRIORITY == -1 then 'selected="selected"' else ""#>Low</option>
<option value="0" #if $sickbeard.PUSHOVER_PRIORITY == 0 then 'selected="selected"' else ""#>Normal</option>
<option value="1" #if $sickbeard.PUSHOVER_PRIORITY == 1 then 'selected="selected"' else ""#>High</option>
</select>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">priority of Pushover messages from SickGear.</span>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">overview: <a href="<%= anon_url('https://pushover.net/api#priority') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">https://pushover.net/api#priority</a></span>
</label>
</div>
<div class="field-pair">
<label for="pushover_device">
<span class="component-title">Pushover device</span>
<input type="hidden" name="pushover_device" id="pushover_device" value="$sickbeard.PUSHOVER_DEVICE" size="35" />
<select id="pushover_device_list" name="pushover_device_list" class="pull-left form-control input-sm" style="margin-right: 5px;">
</select>
<input type="button" class="btn btn-inline" value="Refresh Devices" id="getPushoverDevices" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">which device do you want to push to?</span>
</label>
</div>
<div class="field-pair">
<label for="pushover_sound">
<span class="component-title">Pushover sound</span>
<select id="pushover_sound" name="pushover_sound" class="form-control input-sm">
<option value="pushover" #if $sickbeard.PUSHOVER_SOUND == "pushover" then 'selected="selected"' else ""#>Pushover (default)</option>
<option value="bike" #if $sickbeard.PUSHOVER_SOUND == "bike" then 'selected="selected"' else ""#>Bike</option>
<option value="bugle" #if $sickbeard.PUSHOVER_SOUND == "bugle" then 'selected="selected"' else ""#>Bugle</option>
<option value="cashregister" #if $sickbeard.PUSHOVER_SOUND == "cashregister" then 'selected="selected"' else ""#>Cash Register</option>
<option value="classical" #if $sickbeard.PUSHOVER_SOUND == "classical" then 'selected="selected"' else ""#>Classical</option>
<option value="cosmic" #if $sickbeard.PUSHOVER_SOUND == "cosmic" then 'selected="selected"' else ""#>Cosmic</option>
<option value="falling" #if $sickbeard.PUSHOVER_SOUND == "falling" then 'selected="selected"' else ""#>Falling</option>
<option value="gamelan" #if $sickbeard.PUSHOVER_SOUND == "gamelan" then 'selected="selected"' else ""#>Gamelan</option>
<option value="incoming" #if $sickbeard.PUSHOVER_SOUND == "incoming" then 'selected="selected"' else ""#>Incoming</option>
<option value="intermission" #if $sickbeard.PUSHOVER_SOUND == "intermission" then 'selected="selected"' else ""#>Intermission</option>
<option value="magic" #if $sickbeard.PUSHOVER_SOUND == "magic" then 'selected="selected"' else ""#>Magic</option>
<option value="mechanical" #if $sickbeard.PUSHOVER_SOUND == "mechanical" then 'selected="selected"' else ""#>Mechanical</option>
<option value="pianobar" #if $sickbeard.PUSHOVER_SOUND == "pianobar" then 'selected="selected"' else ""#>Piano Bar</option>
<option value="siren" #if $sickbeard.PUSHOVER_SOUND == "siren" then 'selected="selected"' else ""#>Siren</option>
<option value="spacealarm" #if $sickbeard.PUSHOVER_SOUND == "spacealarm" then 'selected="selected"' else ""#>Space Alarm</option>
<option value="tugboat" #if $sickbeard.PUSHOVER_SOUND == "tugboat" then 'selected="selected"' else ""#>Tug Boat</option>
<option value="alien" #if $sickbeard.PUSHOVER_SOUND == "alien" then 'selected="selected"' else ""#>Alien Alarm (long)</option>
<option value="climb" #if $sickbeard.PUSHOVER_SOUND == "climb" then 'selected="selected"' else ""#>Climb (long)</option>
<option value="persistent" #if $sickbeard.PUSHOVER_SOUND == "persistent" then 'selected="selected"' else ""#>Persistent (long)</option>
<option value="echo" #if $sickbeard.PUSHOVER_SOUND == "echo" then 'selected="selected"' else ""#>Pushover Echo (long)</option>
<option value="updown" #if $sickbeard.PUSHOVER_SOUND == "updown" then 'selected="selected"' else ""#>Up Down (long)</option>
<option value="none" #if $sickbeard.PUSHOVER_SOUND == "none" then 'selected="selected"' else ""#>None (silent)</option>
</select>
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">select one of the predefined sounds for your notifications</span>
</label>
</div>
<div class="testNotification" id="testPushover-result">Click below to test.</div>
<input class="btn" type="button" value="Test Pushover" id="testPushover" />
<input class="btn" type="button" value="Test Pushover" id="testPushover" />
<input type="submit" class="config_submitter btn" value="Save Changes" />
</div><!-- /content_use_pushover //-->
@ -916,7 +1101,7 @@
<div class="field-pair">
<label for="boxcar2_accesstoken">
<span class="component-title">Boxcar2 access token</span>
<input type="text" name="boxcar2_accesstoken" id="boxcar2_accesstoken" value="$sickbeard.BOXCAR2_ACCESSTOKEN" class="form-control input-sm input250" />
<input type="text" name="boxcar2_accesstoken" id="boxcar2_accesstoken" value="<%= starify(sickbeard.BOXCAR2_ACCESSTOKEN) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -1020,7 +1205,7 @@
<div class="field-pair">
<label for="nma_api">
<span class="component-title">NMA API key:</span>
<input type="text" name="nma_api" id="nma_api" value="$sickbeard.NMA_API" class="form-control input-sm input350" />
<input type="text" name="nma_api" id="nma_api" value="<%= starify(sickbeard.NMA_API) %>" class="form-control input-sm input350" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -1099,7 +1284,7 @@
<div class="field-pair">
<label for="pushalot_authorizationtoken">
<span class="component-title">Pushalot authorization token</span>
<input type="text" name="pushalot_authorizationtoken" id="pushalot_authorizationtoken" value="$sickbeard.PUSHALOT_AUTHORIZATIONTOKEN" class="form-control input-sm input350" />
<input type="text" name="pushalot_authorizationtoken" id="pushalot_authorizationtoken" value="<%= starify(sickbeard.PUSHALOT_AUTHORIZATIONTOKEN) %>" class="form-control input-sm input350" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -1118,7 +1303,7 @@
<div class="component-group-desc">
<img class="notifier-icon" src="$sbRoot/images/notifiers/pushbullet.png" alt="" title="Pushbullet" />
<h3><a href="<%= anon_url('https://www.pushbullet.com') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">Pushbullet</a></h3>
<p>Pushbullet is a platform for receiving custom push notifications to connected devices running Android and desktop Chrome browsers.</p>
<p>Pushbullet allows you to send push notifications to Android, iOS and supported browsers.</p>
</div>
<fieldset class="component-group-list">
<div class="field-pair">
@ -1160,25 +1345,26 @@
</label>
</div>
<div class="field-pair">
<label for="pushbullet_api">
<span class="component-title">Pushbullet API key</span>
<input type="text" name="pushbullet_api" id="pushbullet_api" value="$sickbeard.PUSHBULLET_API" class="form-control input-sm input350" />
<label for="pushbullet_access_token">
<span class="component-title">Pushbullet access token</span>
<input type="text" name="pushbullet_access_token" id="pushbullet_access_token" value="<%= starify(sickbeard.PUSHBULLET_ACCESS_TOKEN) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">API key of your Pushbullet account</span>
<span class="component-desc">get your token at: <a href="<%= anon_url('https://www.pushbullet.com/account') %>" rel="noreferrer" onclick="window.open('${sickbeard.ANON_REDIRECT}' + this.href, '_blank'); return false;">https://www.pushbullet.com/account</a></span>
</label>
</div>
<div class="field-pair">
<label for="pushbullet_device_list">
<span class="component-title">Pushbullet devices</span>
<select name="pushbullet_device_list" id="pushbullet_device_list" class="form-control input-sm"></select>
<input type="hidden" id="pushbullet_device" value="$sickbeard.PUSHBULLET_DEVICE">
<input type="button" class="btn btn-inline" value="Update device list" id="getPushbulletDevices" />
<label for="pushbullet_device_iden">
<span class="component-title">Pushbullet device</span>
<input type="hidden" name="pushbullet_device_iden" id="pushbullet_device_iden" value="$sickbeard.PUSHBULLET_DEVICE_IDEN" size="35" />
<select id="pushbullet_device_list" name="pushbullet_device_list" class="pull-left form-control input-sm" style="margin-right: 5px;">
</select>
<input type="button" class="btn btn-inline" value="Refresh Devices" id="getPushbulletDevices" />
</label>
<label>
<span class="component-title">&nbsp;</span>
<span class="component-desc">select device you wish to push to.</span>
<span class="component-desc">which device do you want to push to?</span>
</label>
</div>
<div class="testNotification" id="testPushbullet-result">Click below to test.</div>
@ -1307,7 +1493,7 @@
<div class="field-pair">
<label for="trakt_password">
<span class="component-title">Trakt password</span>
<input type="password" name="trakt_password" id="trakt_password" value="$sickbeard.TRAKT_PASSWORD" class="form-control input-sm input250" />
<input type="password" name="trakt_password" id="trakt_password" value="#echo '*' * len($sickbeard.TRAKT_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -1317,7 +1503,7 @@
<div class="field-pair">
<label for="trakt_api">
<span class="component-title">Trakt API key:</span>
<input type="text" name="trakt_api" id="trakt_api" value="$sickbeard.TRAKT_API" class="form-control input-sm input250" />
<input type="text" name="trakt_api" id="trakt_api" value="<%= starify(sickbeard.TRAKT_API) %>" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>
@ -1501,7 +1687,7 @@
<div class="field-pair">
<label for="email_password">
<span class="component-title">SMTP password</span>
<input type="password" name="email_password" id="email_password" value="$sickbeard.EMAIL_PASSWORD" class="form-control input-sm input250" />
<input type="password" name="email_password" id="email_password" value="#echo '*' * len($sickbeard.EMAIL_PASSWORD)#" class="form-control input-sm input250" />
</label>
<label>
<span class="component-title">&nbsp;</span>

View file

@ -24,10 +24,10 @@
<h1 class="title">$title</h1>
#end if
#set $checked = 'checked="checked"'
#set $selected = 'selected="selected"'
#set $selected = 'selected="selected" class="selected"'
<div id="config" class="pp">
<div id="config-content">
<div id="config-content" class="linefix">
<form id="configForm" action="savePostProcessing" method="post">
@ -50,12 +50,12 @@
<div class="field-pair">
<label for="tv_download_dir">
<span class="component-title">Completed TV downloads
<div class="red-text"><em class="boldest">must not</em> be the folder where downloading files are created</div>
<p class="red-text" style="line-height:20px;margin:0"><em class="boldest">must not</em> be the folder where downloading files are created</p>
</span>
<span class="component-desc">
<input type="text" name="tv_download_dir" id="tv_download_dir" value="$sickbeard.TV_DOWNLOAD_DIR" class="form-control input-sm input350" />
<input type="text" name="tv_download_dir" id="tv_download_dir" value="$sickbeard.TV_DOWNLOAD_DIR" class="form-control input-sm input350">
<div class="pull-left">
<p>folder where download clients save <b><em class="boldest">completed</em></b> downloads.&nbsp;
<p class="note">folder where download clients save <b><em class="boldest">completed</em></b> downloads.&nbsp;
<b>note:</b> only use if not using SABnzbd post processing <em>or</em> if SABnzbd is on a different PC to SickGear</p>
</div>
</span>
@ -67,13 +67,9 @@
<span class="component-title">Process episode method</span>
<span class="component-desc">
<select name="process_method" id="process_method" class="form-control input-sm">
#set $process_method_text = {'copy': "Copy", 'move': "Move", 'hardlink': "Hard Link", 'symlink' : "Symbolic Link"}
#set $process_method_text = {'copy': 'Copy', 'move': 'Move', 'hardlink': 'Hard Link', 'symlink' : 'Symbolic Link'}
#for $curAction in ('copy', 'move', 'hardlink', 'symlink'):
#if $sickbeard.PROCESS_METHOD == $curAction:
#set $process_method = $selected
#else
#set $process_method = ''
#end if
#set $process_method = ('', $selected)[$sickbeard.PROCESS_METHOD == $curAction]
<option value="$curAction" $process_method>$process_method_text[$curAction]</option>
#end for
</select>
@ -86,9 +82,9 @@
<label for="process_automatically">
<span class="component-title">Scan and post process</span>
<span class="component-desc">
<input type="checkbox" name="process_automatically" id="process_automatically" #if $sickbeard.PROCESS_AUTOMATICALLY == True then $checked else ''# />
<p>files in the <em>completed TV downloads</em> folder.&nbsp;
<b>note:</b> do not enable with external post processing scripts like sabToSickbeard for SABnzbd, or NZBMedia for NZBGET</p>
<input type="checkbox" name="process_automatically" id="process_automatically" #if $sickbeard.PROCESS_AUTOMATICALLY == True then $checked else ''#>
<p>files in the <em>completed TV downloads</em> folder</p>
<p class="clear-left note"><b>note:</b> do not enable with external post processing scripts like sabToSickbeard for SABnzbd, or NZBMedia for NZBGET</p>
</span>
</label>
</div>
@ -97,7 +93,7 @@
<label for="autopostprocesser_frequency">
<span class="component-title">Run post process every</span>
<span class="component-desc">
<input type="text" name="autopostprocesser_frequency" id="autopostprocesser_frequency" value="$sickbeard.AUTOPOSTPROCESSER_FREQUENCY" class="form-control input-sm input75" />
<input type="text" name="autopostprocesser_frequency" id="autopostprocesser_frequency" value="$sickbeard.AUTOPOSTPROCESSER_FREQUENCY" class="form-control input-sm input75">
<p>minutes to check for new files to process (e.g. 10)</p>
</span>
</label>
@ -107,8 +103,8 @@
<label for="postpone_if_sync_files">
<span class="component-title">Postpone post processing</span>
<span class="component-desc">
<input type="checkbox" name="postpone_if_sync_files" id="postpone_if_sync_files" #if $sickbeard.POSTPONE_IF_SYNC_FILES == True then $checked else ''# />
<p>if <b>!sync</b> files are present in the <em>completed TV downloads</em> folder</p>
<input type="checkbox" name="postpone_if_sync_files" id="postpone_if_sync_files" #if $sickbeard.POSTPONE_IF_SYNC_FILES == True then $checked else ''#>
<p>if <b class="grey-text">!sync</b> files are present in the <em>completed TV downloads</em> folder</p>
</span>
</label>
</div>
@ -117,17 +113,46 @@
<label for="extra_scripts">
<span class="component-title">Extra scripts</span>
<span class="component-desc">
<input type="text" name="extra_scripts" value="<%='|'.join(sickbeard.EXTRA_SCRIPTS)%>" class="form-control input-sm input350" />
<input type="text" name="extra_scripts" id="extra_scripts" value="<%='|'.join(sickbeard.EXTRA_SCRIPTS)%>" class="form-control input-sm input350">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_extra_params" title="Toggle info for script arguments" class="legend" class="legend" />
<div class="clear-left">
<p>scripts are called after SickGear's built-in post processing.
&nbsp;<b>note:</b> use <b>|</b> to separate additional extra scripts</span>
</p>
<p class="note">scripts are called after built-in post processing.
&nbsp;<b>note:</b> use <b class="grey-text boldest">|</b> to separate additional extra scripts
(e.g. <span class="grey-text">inside_SG_root_folder.sh|/other/path/to/script.py</span>)</p>
<div id="extra_params" style="display: none">
<table class="Key" style="width:auto">
<thead>
<tr>
<th class="text-center">Arg</th>
<th>Description</th>
<th style="width:5%"></th>
<th class="text-center">Arg</th>
<th>Description</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="5">These arguments are passed to every script</th>
</tr>
</tfoot>
<tbody>
<tr><td class="text-center"><span class="grey-text">1</span></td><td>final full episode file path</td>
<td>&nbsp;</td><td class="text-center"><span class="grey-text">4</span></td><td>season number</td></tr>
<tr class="even"><td class="text-center"><span class="grey-text">2</span></td><td>original episode file name</td>
<td>&nbsp;</td><td class="text-center"><span class="grey-text">5</span></td><td style="padding-right:1.5em !important">episode number</td></tr>
<tr><td class="text-center"><span class="grey-text">3</span></td><td>show id</td>
<td>&nbsp;</td><td class="text-center"><span class="grey-text">6</span></td><td>episode air date</td></tr>
</tbody>
</table>
</div>
</div>
</span>
</label>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</fieldset>
</div>
@ -145,7 +170,7 @@
<label for="unpack">
<span class="component-title">Unpack downloads</span>
<span class="component-desc">
<input id="unpack" type="checkbox" name="unpack" #if $sickbeard.UNPACK == True then $checked else ''# />
<input id="unpack" type="checkbox" name="unpack" #if $sickbeard.UNPACK == True then $checked else ''#>
<p>unrar TV releases in the <em>completed TV downloads</em> folder</p>
</span>
</label>
@ -153,11 +178,17 @@
<div class="field-pair">
<label for="skip_removed_files">
<span class="component-title">Skip remove detection</span>
<span class="component-title">Status of removed episodes</span>
<span class="component-desc">
<input type="checkbox" name="skip_removed_files" id="skip_removed_files" #if $sickbeard.SKIP_REMOVED_FILES == True then $checked else ''# />
<p>skip detection of removed files so the episode is not set to ignored<br>
<b>note:</b> this may mean SickGear misses renames as well</p>
<select name="skip_removed_files" id="skip_removed_files" class="form-control form-control-inline input-sm">
#set $opt_selected = ($sickbeard.SKIP_REMOVED_FILES, $IGNORED)[not $sickbeard.SKIP_REMOVED_FILES]
#for $cur_status in [1, $SKIPPED, $ARCHIVED, $IGNORED]:
#set $opt_text = ('Will Be Untouched', 'Set ' + $statusStrings[$cur_status] + ('', ' (default)')[$IGNORED == $cur_status])[1 != $cur_status]
<option value="$cur_status"#echo ('', ' ' + $selected)[$cur_status == $opt_selected]#>$opt_text</option>
#end for
</select>
<span><strong><em>untouched</em></strong> may mean SickGear also misses renames</span>
</span>
</label>
</div>
@ -166,7 +197,7 @@
<label for="move_associated_files">
<span class="component-title">Move associated files</span>
<span class="component-desc">
<input type="checkbox" name="move_associated_files" id="move_associated_files" #if $sickbeard.MOVE_ASSOCIATED_FILES == True then $checked else ''# />
<input type="checkbox" name="move_associated_files" id="move_associated_files" #if $sickbeard.MOVE_ASSOCIATED_FILES == True then $checked else ''#>
<p>move srr/srt/sfv/etc files with the episode when processed</p>
</span>
</label>
@ -176,7 +207,7 @@
<label for="nfo_rename">
<span class="component-title">Rename .nfo file</span>
<span class="component-desc">
<input type="checkbox" name="nfo_rename" id="nfo_rename" #if $sickbeard.NFO_RENAME == True then $checked else ''# />
<input type="checkbox" name="nfo_rename" id="nfo_rename" #if $sickbeard.NFO_RENAME == True then $checked else ''#>
<p>rename the original .nfo file to .nfo-orig to avoid conflicts</p>
</span>
</label>
@ -186,7 +217,7 @@
<label for="rename_episodes">
<span class="component-title">Rename episodes</span>
<span class="component-desc">
<input type="checkbox" name="rename_episodes" id="rename_episodes" #if $sickbeard.RENAME_EPISODES == True then $checked else ''# />
<input type="checkbox" name="rename_episodes" id="rename_episodes" #if $sickbeard.RENAME_EPISODES == True then $checked else ''#>
<p>rename episodes using the Episode Naming settings</p>
</span>
</label>
@ -196,14 +227,14 @@
<label for="airdate_episodes">
<span class="component-title">Change file date</span>
<span class="component-desc">
<input type="checkbox" name="airdate_episodes" id="airdate_episodes" #if $sickbeard.AIRDATE_EPISODES == True then $checked else ''# />
<p>set last modified filedate to the date that the episode aired<br>
<b>note:</b> some systems may ignore this feature.</p>
<input type="checkbox" name="airdate_episodes" id="airdate_episodes" #if $sickbeard.AIRDATE_EPISODES == True then $checked else ''#>
<p>set last modified filedate to the date that the episode aired</p>
<p class="note"><b>note:</b> some systems may ignore this feature.</p>
</span>
</label>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</fieldset>
</div>
@ -221,7 +252,7 @@
<label for="use_failed_downloads" style="margin-bottom:0">
<span class="component-title">Enable</span>
<span class="component-desc">
<input id="use_failed_downloads" type="checkbox" class="enabler" name="use_failed_downloads" #if $sickbeard.USE_FAILED_DOWNLOADS == True then $checked else ''# />
<input id="use_failed_downloads" type="checkbox" class="enabler" name="use_failed_downloads" #if $sickbeard.USE_FAILED_DOWNLOADS == True then $checked else ''#>
<p>failed download handling</p>
</span>
</label>
@ -231,10 +262,10 @@
<div class="field-pair" style="padding-top:0">
<span class="component-desc">
<ul id="failed-guide">
<li class="title">SABnzbd setup guide</li>
<li class="action">menu "Switches"&nbsp;... [disable] "<em class="boldest">Abort jobs that cannot be completed</em>"</li>
<li class="action">menu "Switches"&nbsp;... [disable] "<em class="boldest">Post-Process Only Verified Jobs</em>"</li>
<li class="action">menu "Special"&nbsp;... [enable] "<em class="boldest">empty_postproc</em>"</li>
<li class="title note">SABnzbd setup guide</li>
<li class="action note">menu "Switches"&nbsp;... [disable] "<em class="boldest">Abort jobs that cannot be completed</em>"</li>
<li class="action note">menu "Switches"&nbsp;... [disable] "<em class="boldest">Post-Process Only Verified Jobs</em>"</li>
<li class="action note">menu "Special"&nbsp;... [enable] "<em class="boldest">empty_postproc</em>"</li>
</ul>
</span>
</div>
@ -243,14 +274,14 @@
<label for="delete_failed">
<span class="component-title">Delete failed downloads</span>
<span class="component-desc">
<input id="delete_failed" type="checkbox" name="delete_failed" #if $sickbeard.DELETE_FAILED == True then $checked else ''# />
<p>delete left over files from a failed download<br>
<input id="delete_failed" type="checkbox" name="delete_failed" #if $sickbeard.DELETE_FAILED == True then $checked else ''#>
<p>delete left over files from a failed download<br />
</span>
</label>
</div>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</fieldset>
</div>
@ -300,7 +331,7 @@
<label for="naming_pattern">
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_pattern" id="naming_pattern" value="$sickbeard.NAMING_PATTERN" class="form-control input-sm input350 custom-pattern" />
<input type="text" name="naming_pattern" id="naming_pattern" value="$sickbeard.NAMING_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend" class="legend" class="legend" />
</span>
</label>
@ -407,23 +438,23 @@
<td>720p_BluRay</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="Multi-EP style is ignored"></i> <b>Release Name:</b></td>
<td class="align-right"><i class="sgicon-info" title="Multi-EP style is ignored"></i> <b>Release Name:</b></td>
<td>%RN</td>
<td>Show.Name.S02E03.HDTV.XviD-RLSGROUP</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="'SickGear' is used in place of RLSGROUP if it could not be properly detected"></i> <b>Release Group:</b></td>
<td>%RG</td>
<td class="align-right"><i class="sgicon-info" title="'SickGear' is used in place of RLSGROUP if it could not be properly detected"></i> <b>Release Group:</b></td>
<td>-%RG</td>
<td>RLSGROUP</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="If episode is proper/repack add 'proper' to name."></i> <b>Release Type:</b></td>
<td class="align-right"><i class="sgicon-info" title="If episode is proper/repack add 'proper' to name."></i> <b>Release Type:</b></td>
<td>%RT</td>
<td>PROPER</td>
</tr>
</tbody>
</table>
<br/>
<br />
</div>
</div>
@ -433,7 +464,7 @@
<span class="component-desc">
<select id="naming_multi_ep" name="naming_multi_ep" class="form-control input-sm">
#for $cur_multi_ep in sorted($multiEpStrings.items(), key=lambda x: x[1]):
<option value="$cur_multi_ep[0]" #if $sickbeard.NAMING_MULTI_EP == $cur_multi_ep[0] then 'selected="selected" class="selected"' else ''#>$cur_multi_ep[1]</option>
<option value="$cur_multi_ep[0]" #if $sickbeard.NAMING_MULTI_EP == $cur_multi_ep[0] then $selected else ''#>$cur_multi_ep[1]</option>
#end for
</select>
</span>
@ -461,7 +492,7 @@
<div class="example">
<span class="jumbo" id="naming_example">&nbsp;</span>
</div>
<br/>
<br />
</div>
<div id="naming_example_multi_div">
@ -471,7 +502,7 @@
<div class="example">
<span class="jumbo" id="naming_example_multi">&nbsp;</span>
</div>
<br/>
<br />
</div>
</fieldset>
@ -520,7 +551,7 @@
<label for="naming_abd_pattern">
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_abd_pattern" id="naming_abd_pattern" value="$sickbeard.NAMING_ABD_PATTERN" class="form-control input-sm input350 custom-pattern" />
<input type="text" name="naming_abd_pattern" id="naming_abd_pattern" value="$sickbeard.NAMING_ABD_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend" class="legend" />
</span>
</label>
@ -632,23 +663,23 @@
<td>09</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td class="align-right"><i class="sgicon-info" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td>%RN</td>
<td>Show.Name.2010.03.09.HDTV.XviD-RLSGROUP</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="if RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>%RG</td>
<td class="align-right"><i class="sgicon-info" title="if RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>-%RG</td>
<td>RLSGROUP</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="Add 'proper' to name if episode is a proper or repack">&nbsp;</i> <b>Release Type:</b></td>
<td class="align-right"><i class="sgicon-info" title="Add 'proper' to name if episode is a proper or repack"></i> <b>Release Type:</b></td>
<td>%RT</td>
<td>PROPER</td>
</tr>
</tbody>
</table>
<br/>
<br />
</div>
</div><!-- /naming_abd_custom -->
</div>
@ -663,13 +694,13 @@
<div class="example">
<span class="jumbo" id="naming_abd_example">&nbsp;</span>
</div>
<br/>
<br />
</div>
</div>
</fieldset>
<div class="field-pair right">
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div>
</div><!-- /naming_abd_different -->
@ -718,7 +749,7 @@
<label for="naming_sports_pattern">
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_sports_pattern" id="naming_sports_pattern" value="$sickbeard.NAMING_SPORTS_PATTERN" class="form-control input-sm input350 custom-pattern" />
<input type="text" name="naming_sports_pattern" id="naming_sports_pattern" value="$sickbeard.NAMING_SPORTS_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_sports_key" title="Toggle Sports Naming Legend" class="legend" />
</span>
</label>
@ -830,23 +861,23 @@
<td>09</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td class="align-right"><i class="sgicon-info" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td>%RN</td>
<td>Show.Name.9th.Mar.2011.HDTV.XviD-RLSGROUP</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="If RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>%RG</td>
<td class="align-right"><i class="sgicon-info" title="If RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>-%RG</td>
<td>RLSGROUP</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="Add 'proper' to name if episode is a proper or repack"></i> <b>Release Type:</b></td>
<td class="align-right"><i class="sgicon-info" title="Add 'proper' to name if episode is a proper or repack"></i> <b>Release Type:</b></td>
<td>%RT</td>
<td>PROPER</td>
</tr>
</tbody>
</table>
<br/>
<br />
</div>
</div><!-- /naming_sports_custom -->
</div>
@ -863,7 +894,7 @@
<div class="example">
<span class="jumbo" id="naming_sports_example">&nbsp;</span>
</div>
<br/>
<br />
</div>
</div>
@ -871,7 +902,7 @@
</fieldset>
<div class="field-pair right">
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div>
</div><!-- /naming_sports_different -->
@ -921,7 +952,7 @@
<label for="naming_anime_pattern">
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_anime_pattern" id="naming_anime_pattern" value="$sickbeard.NAMING_ANIME_PATTERN" class="form-control input-sm input350 custom-pattern" />
<input type="text" name="naming_anime_pattern" id="naming_anime_pattern" value="$sickbeard.NAMING_ANIME_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_anime_key" title="Toggle Anime Naming Legend" class="legend" />
</span>
</label>
@ -1028,23 +1059,23 @@
<td>720p_BluRay</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td class="align-right"><i class="sgicon-info" title="Multi-episode style is ignored"></i> <b>Release Name:</b></td>
<td>%RN</td>
<td>Show.Name.S02E03.HDTV.XviD-RLSGROUP</td>
</tr>
<tr>
<td class="align-right"><i class="icon-info-sign" title="If RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>%RG</td>
<td class="align-right"><i class="sgicon-info" title="If RLSGROUP is not detected, 'SickGear' is used"></i> <b>Release Group:</b></td>
<td>[%RG]</td>
<td>RLSGROUP</td>
</tr>
<tr class="even">
<td class="align-right"><i class="icon-info-sign" title="Add 'proper' to name if episode is a proper or repack"></i> <b>Release Type:</b></td>
<td class="align-right"><i class="sgicon-info" title="Add 'proper' to name if episode is a proper or repack"></i> <b>Release Type:</b></td>
<td>%RT</td>
<td>PROPER</td>
</tr>
</tbody>
</table>
<br/>
<br />
</div>
</div><!-- /naming_anime_custom -->
@ -1054,7 +1085,7 @@
<span class="component-desc">
<select id="naming_anime_multi_ep" name="naming_anime_multi_ep" class="form-control input-sm">
#for $cur_multi_ep in sorted($multiEpStrings.items(), key=lambda x: x[1]):
<option value="$cur_multi_ep[0]" #if $sickbeard.NAMING_ANIME_MULTI_EP == $cur_multi_ep[0] then 'selected="selected" class="selected"' else ''#>$cur_multi_ep[1]</option>
<option value="$cur_multi_ep[0]" #if $sickbeard.NAMING_ANIME_MULTI_EP == $cur_multi_ep[0] then $selected else ''#>$cur_multi_ep[1]</option>
#end for
</select>
</span>
@ -1074,7 +1105,7 @@
<div class="example">
<span class="jumbo" id="naming_example_anime">&nbsp;</span>
</div>
<br/>
<br />
</div>
<div id="naming_example_multi_anime_div">
@ -1084,7 +1115,7 @@
<div class="example">
<span class="jumbo" id="naming_example_multi_anime">&nbsp;</span>
</div>
<br/>
<br />
</div>
</fieldset>
@ -1096,8 +1127,8 @@
<span class="component-title">Add absolute numbering</span>
<span class="component-desc">
<input type="radio" name="naming_anime" id="naming_anime" value="1" #if $sickbeard.NAMING_ANIME == 1 then $checked else ''#/>
<p>add the absolute number to the season/episode format<br>
(eg. S15E45 - 310 vs S15E45)</p>
<p>add the absolute number to the season/episode format</p>
<p class="note">(eg. S15E45 - 310 vs S15E45)</p>
</span>
</label>
</div>
@ -1125,7 +1156,7 @@
</fieldset>
<div class="field-pair right">
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div>
</div><!-- /naming_anime_different -->
@ -1202,18 +1233,18 @@
<label for="${cur_id}_season_all_banner"><span id="${cur_id}_eg_season_all_banner">$cur_metadata_inst.eg_season_all_banner</span></label>
</div>
</div>
<input type="hidden" name="${cur_id}_data" id="${cur_id}_data" value="$cur_metadata_inst.get_config()" />
<input type="hidden" name="${cur_id}_data" id="${cur_id}_data" value="$cur_metadata_inst.get_config()">
</div>
#end for
<div class="clearfix"></div><br/>
<div class="clearfix"></div><br />
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</fieldset>
</div><!-- /component-group3 //-->
<br/>
<br />
<h6 class="pull-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b> </h6>
<input type="submit" class="btn pull-left config_submitter button" value="Save Changes" />
<input type="submit" class="btn pull-left config_submitter button" value="Save Changes">
</form>
</div>

View file

@ -1,7 +1,7 @@
#import sickbeard
#from sickbeard.providers.generic import GenericProvider
#from sickbeard.providers import thepiratebay
#from sickbeard.helpers import anon_url
#from sickbeard.helpers import anon_url, starify
#set global $title="Config - Providers"
#set global $header="Search Providers"
@ -43,7 +43,7 @@
#for $curNewznabProvider in $sickbeard.newznabProviderList:
\$(this).addProvider('$curNewznabProvider.getID()', '$curNewznabProvider.name', '$curNewznabProvider.url', '$curNewznabProvider.key', '$curNewznabProvider.catIDs', $int($curNewznabProvider.default), show_nzb_providers);
\$(this).addProvider('$curNewznabProvider.getID()', '$curNewznabProvider.name', '$curNewznabProvider.url', '<%= starify(curNewznabProvider.key) %>', '$curNewznabProvider.catIDs', $int($curNewznabProvider.default), show_nzb_providers);
#end for
@ -53,7 +53,7 @@
#for $curTorrentRssProvider in $sickbeard.torrentRssProviderList:
\$(this).addTorrentRssProvider('$curTorrentRssProvider.getID()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '$curTorrentRssProvider.cookies');
\$(this).addTorrentRssProvider('$curTorrentRssProvider.getID()', '$curTorrentRssProvider.name', '$curTorrentRssProvider.url', '<%= starify(curTorrentRssProvider.cookies) %>');
#end for
@ -207,7 +207,7 @@
<label for="${curNewznabProvider.getID()}_hash">
<span class="component-title">API key</span>
<span class="component-desc">
<input type="text" id="${curNewznabProvider.getID()}_hash" value="$curNewznabProvider.key" newznab_name="${curNewznabProvider.getID()}_hash" class="newznab_key form-control input-sm input350" />
<input type="text" id="${curNewznabProvider.getID()}_hash" value="<%= starify(curNewznabProvider.key) %>" newznab_name="${curNewznabProvider.getID()}_hash" class="newznab_key form-control input-sm input350" />
<div class="clear-left"><p>get API key from provider website</p></div>
</span>
</label>
@ -285,7 +285,7 @@
<label for="${curNzbProvider.getID()}_api_key">
<span class="component-title">API key</span>
<span class="component-desc">
<input type="text" name="${curNzbProvider.getID()}_api_key" value="$curNzbProvider.api_key" class="form-control input-sm input350" />
<input type="text" name="${curNzbProvider.getID()}_api_key" value="<%= starify(curNzbProvider.api_key) %>" class="form-control input-sm input350" />
</span>
</label>
</div>
@ -361,7 +361,7 @@
<label for="${curTorrentProvider.getID()}_api_key">
<span class="component-title">Api key:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_api_key" id="${curTorrentProvider.getID()}_api_key" value="$curTorrentProvider.api_key" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.getID()}_api_key" id="${curTorrentProvider.getID()}_api_key" value="<%= starify(curTorrentProvider.api_key) %>" class="form-control input-sm input350" />
</span>
</label>
</div>
@ -405,7 +405,7 @@
<label for="${curTorrentProvider.getID()}_password">
<span class="component-title">Password:</span>
<span class="component-desc">
<input type="password" name="${curTorrentProvider.getID()}_password" id="${curTorrentProvider.getID()}_password" value="$curTorrentProvider.password" class="form-control input-sm input350" />
<input type="password" name="${curTorrentProvider.getID()}_password" id="${curTorrentProvider.getID()}_password" value="#echo '*' * len($curTorrentProvider.password)#" class="form-control input-sm input350" />
</span>
</label>
</div>
@ -416,7 +416,7 @@
<label for="${curTorrentProvider.getID()}_passkey">
<span class="component-title">Passkey:</span>
<span class="component-desc">
<input type="text" name="${curTorrentProvider.getID()}_passkey" id="${curTorrentProvider.getID()}_passkey" value="$curTorrentProvider.passkey" class="form-control input-sm input350" />
<input type="text" name="${curTorrentProvider.getID()}_passkey" id="${curTorrentProvider.getID()}_passkey" value="<%= starify(curTorrentProvider.passkey) %>" class="form-control input-sm input350" />
</span>
</label>
</div>

View file

@ -1,5 +1,7 @@
#import sickbeard
#from sickbeard import clients
#from sickbeard.helpers import starify
#set global $title = 'Config - Episode Search'
#set global $header = 'Search Settings'
@ -19,10 +21,10 @@
#end if
#set $html_selected = ' selected="selected"'
#set $html_checked = 'checked="checked" '
#set $html_checked = ' checked="checked"'
<div id="config">
<div id="config-content">
<div id="config-content" class="linefix">
<form id="configForm" action="saveSearch" method="post">
@ -46,7 +48,7 @@
<label for="download_propers">
<span class="component-title">Download propers</span>
<span class="component-desc">
<input type="checkbox" name="download_propers" id="download_propers" class="enabler" <%= html_checked if sickbeard.DOWNLOAD_PROPERS == True else '' %>/>
<input type="checkbox" name="download_propers" id="download_propers" class="enabler"<%= html_checked if sickbeard.DOWNLOAD_PROPERS == True else '' %>>
<p>replace original download with "Proper" or "Repack" if nuked</p>
</span>
</label>
@ -70,10 +72,20 @@
<div class="field-pair">
<label>
<span class="component-title">Backlog search day(s)</span>
<span class="component-title">Recent search frequency</span>
<span class="component-desc">
<input type="text" name="backlog_days" value="$sickbeard.BACKLOG_DAYS" class="form-control input-sm input75" />
<p>number of day(s) that the search will cover (e.g. 7)</p>
<input type="text" name="recentsearch_frequency" value="$sickbeard.RECENTSEARCH_FREQUENCY" class="form-control input-sm input75">
<p>minutes between checking recent updated shows (minimum $sickbeard.MIN_RECENTSEARCH_FREQUENCY)</p>
</span>
</label>
</div>
<div class="field-pair">
<label>
<span class="component-title">Limited backlog search</span>
<span class="component-desc">
<input type="text" name="backlog_days" value="$sickbeard.BACKLOG_DAYS" class="form-control input-sm input75">
<p>day(s) that the limited search will cover (e.g. 7)</p>
</span>
</label>
</div>
@ -82,27 +94,17 @@
<label>
<span class="component-title">Backlog search frequency</span>
<span class="component-desc">
<input type="text" name="backlog_frequency" value="$sickbeard.BACKLOG_FREQUENCY" class="form-control input-sm input75" />
<p>time in minutes between searches (min. $sickbeard.MIN_BACKLOG_FREQUENCY)</p>
<input type="text" name="backlog_frequency" value="$sickbeard.BACKLOG_FREQUENCY" class="form-control input-sm input75">
<p>minutes between searches (minimum $sickbeard.MIN_BACKLOG_FREQUENCY)</p>
</span>
</label>
</div>
<div class="field-pair">
<label>
<span class="component-title">Recent search frequency</span>
<span class="component-desc">
<input type="text" name="recentsearch_frequency" value="$sickbeard.RECENTSEARCH_FREQUENCY" class="form-control input-sm input75" />
<p>time in minutes between searches (min. $sickbeard.MIN_RECENTSEARCH_FREQUENCY)</p>
</span>
</label>
</div>
<div class="field-pair">
<label>
<span class="component-title">Usenet retention</span>
<span class="component-desc">
<input type="text" name="usenet_retention" value="$sickbeard.USENET_RETENTION" class="form-control input-sm input75" />
<input type="text" name="usenet_retention" value="$sickbeard.USENET_RETENTION" class="form-control input-sm input75">
<p>age limit in days for usenet articles to be used (e.g. 500)</p>
</span>
</label>
@ -112,8 +114,8 @@
<label>
<span class="component-title">Ignore result with any word</span>
<span class="component-desc">
<input type="text" name="ignore_words" value="$sickbeard.IGNORE_WORDS" class="form-control input-sm input350" />
<div class="clear-left">ignore search result <em class="grey-text">if its title contains any</em> of these comma seperated words</div>
<input type="text" name="ignore_words" value="$sickbeard.IGNORE_WORDS" class="form-control input-sm input350">
<p class="clear-left note">ignore search result <em class="grey-text">if its title contains any</em> of these comma seperated words</p>
</span>
</label>
</div>
@ -122,8 +124,8 @@
<label>
<span class="component-title">Require at least one word</span>
<span class="component-desc">
<input type="text" name="require_words" value="$sickbeard.REQUIRE_WORDS" class="form-control input-sm input350" />
<div class="clear-left">ignore search result <em class="grey-text">unless its title contains one</em> of these comma seperated words</div>
<input type="text" name="require_words" value="$sickbeard.REQUIRE_WORDS" class="form-control input-sm input350">
<p class="clear-left note">ignore search result <em class="grey-text">unless its title contains one</em> of these comma seperated words</p>
</span>
</label>
</div>
@ -132,13 +134,13 @@
<label for="allow_high_priority">
<span class="component-title">Allow high priority</span>
<span class="component-desc">
<input type="checkbox" name="allow_high_priority" id="allow_high_priority" <%= html_checked if sickbeard.ALLOW_HIGH_PRIORITY == True else '' %>/>
<input type="checkbox" name="allow_high_priority" id="allow_high_priority"<%= html_checked if sickbeard.ALLOW_HIGH_PRIORITY == True else '' %>>
<p>set downloads of recently aired episodes to high priority</p>
</span>
</label>
</div>
<input type="submit" class="btn config_submitter" value="Save Changes" />
<input type="submit" class="btn config_submitter" value="Save Changes">
</fieldset>
</div><!-- /component-group1 //-->
@ -156,7 +158,7 @@
<label for="use_nzbs">
<span class="component-title">Search NZBs</span>
<span class="component-desc">
<input type="checkbox" name="use_nzbs" class="enabler" id="use_nzbs" <%= html_checked if sickbeard.USE_NZBS else '' %>/>
<input type="checkbox" name="use_nzbs" class="enabler" id="use_nzbs"<%= html_checked if sickbeard.USE_NZBS else '' %>>
<p>enable NZB search providers</p></span>
</label>
</div>
@ -182,8 +184,8 @@
<label>
<span class="component-title">Black hole folder location</span>
<span class="component-desc">
<input type="text" name="nzb_dir" id="nzb_dir" value="$sickbeard.NZB_DIR" class="form-control input-sm input350" />
<div class="clear-left"><p><b>.nzb</b> files are stored at this location for external software to find and use</p></div>
<input type="text" name="nzb_dir" id="nzb_dir" value="$sickbeard.NZB_DIR" class="form-control input-sm input350">
<p class="clear-left note"><b>.nzb</b> files are stored at this location for external software to find and use</p>
</span>
</label>
</div>
@ -194,8 +196,8 @@
<label>
<span class="component-title">SABnzbd server URL</span>
<span class="component-desc">
<input type="text" id="sab_host" name="sab_host" value="$sickbeard.SAB_HOST" class="form-control input-sm input350" />
<div class="clear-left"><p>URL to your SABnzbd server (e.g. http://localhost:8080/)</p></div>
<input type="text" id="sab_host" name="sab_host" value="$sickbeard.SAB_HOST" class="form-control input-sm input350">
<p class="clear-left note">URL to your SABnzbd server (e.g. http://localhost:8080/)</p>
</span>
</label>
</div>
@ -204,7 +206,7 @@
<label>
<span class="component-title">SABnzbd username</span>
<span class="component-desc">
<input type="text" name="sab_username" id="sab_username" value="$sickbeard.SAB_USERNAME" class="form-control input-sm input200" />
<input type="text" name="sab_username" id="sab_username" value="$sickbeard.SAB_USERNAME" class="form-control input-sm input200">
<p>(blank for none)</p>
</span>
</label>
@ -214,7 +216,7 @@
<label>
<span class="component-title">SABnzbd password</span>
<span class="component-desc">
<input type="password" name="sab_password" id="sab_password" value="$sickbeard.SAB_PASSWORD" class="form-control input-sm input200" />
<input type="password" name="sab_password" id="sab_password" value="#echo '*' * len($sickbeard.SAB_PASSWORD)#" class="form-control input-sm input200">
<p>(blank for none)</p>
</span>
</label>
@ -224,8 +226,8 @@
<label>
<span class="component-title">SABnzbd API key</span>
<span class="component-desc">
<input type="text" name="sab_apikey" id="sab_apikey" value="$sickbeard.SAB_APIKEY" class="form-control input-sm input350" />
<div class="clear-left"><p>locate at... SABnzbd Config -> General -> API Key</p></div>
<input type="text" name="sab_apikey" id="sab_apikey" value="<%= starify(sickbeard.SAB_APIKEY) %>" class="form-control input-sm input350">
<p class="clear-left note">locate at... SABnzbd Config -> General -> API Key</p>
</span>
</label>
</div>
@ -234,7 +236,7 @@
<label>
<span class="component-title">Use SABnzbd category</span>
<span class="component-desc">
<input type="text" name="sab_category" id="sab_category" value="$sickbeard.SAB_CATEGORY" class="form-control input-sm input200" />
<input type="text" name="sab_category" id="sab_category" value="$sickbeard.SAB_CATEGORY" class="form-control input-sm input200">
<p>add downloads to this category (e.g. TV)</p>
</span>
</label>
@ -246,7 +248,7 @@
<label for="nzbget_use_https">
<span class="component-title">Connect using HTTPS</span>
<span class="component-desc">
<input id="nzbget_use_https" type="checkbox" class="enabler" name="nzbget_use_https" <%= html_checked if sickbeard.NZBGET_USE_HTTPS == True else '' %>/>
<input id="nzbget_use_https" type="checkbox" class="enabler" name="nzbget_use_https"<%= html_checked if sickbeard.NZBGET_USE_HTTPS == True else '' %>>
<p><b>note:</b> enable Secure control in NZBGet and set the correct Secure Port here</p>
</span>
</label>
@ -257,9 +259,9 @@
<label>
<span class="component-title">NZBget host:port</span>
<span class="component-desc">
<input type="text" name="nzbget_host" id="nzbget_host" value="$sickbeard.NZBGET_HOST" class="form-control input-sm input350" />
<input type="text" name="nzbget_host" id="nzbget_host" value="$sickbeard.NZBGET_HOST" class="form-control input-sm input350">
<p>(e.g. localhost:6789)</p>
<div class="clear-left"><p>NZBget RPC host name and port number (not NZBgetweb!)</p></div>
<p class="clear-left note">NZBget RPC host name and port number (not NZBgetweb!)</p>
</span>
</label>
</div>
@ -268,7 +270,7 @@
<label>
<span class="component-title">NZBget username</span>
<span class="component-desc">
<input type="text" name="nzbget_username" value="$sickbeard.NZBGET_USERNAME" class="form-control input-sm input200" />
<input type="text" name="nzbget_username" value="$sickbeard.NZBGET_USERNAME" class="form-control input-sm input200">
<p>locate in nzbget.conf (default:nzbget)</p>
</span>
</label>
@ -278,7 +280,7 @@
<label>
<span class="component-title">NZBget password</span>
<span class="component-desc">
<input type="password" name="nzbget_password" id="nzbget_password" value="$sickbeard.NZBGET_PASSWORD" class="form-control input-sm input200" />
<input type="password" name="nzbget_password" id="nzbget_password" value="#echo '*' * len($sickbeard.NZBGET_PASSWORD)#" class="form-control input-sm input200">
<p>locate in nzbget.conf (default:tegbzn6789)</p>
</span>
</label>
@ -288,7 +290,7 @@
<label>
<span class="component-title">Use NZBget category</span>
<span class="component-desc">
<input type="text" name="nzbget_category" id="nzbget_category" value="$sickbeard.NZBGET_CATEGORY" class="form-control input-sm input200" />
<input type="text" name="nzbget_category" id="nzbget_category" value="$sickbeard.NZBGET_CATEGORY" class="form-control input-sm input200">
<p>send downloads marked this category (e.g. TV)</p>
</span>
</label>
@ -334,8 +336,8 @@
</div>
<div class="testNotification" id="testSABnzbd_result">Click below to test</div>
<input class="btn" type="button" value="Test SABnzbd" id="testSABnzbd" class="btn test-button"/>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input class="btn" type="button" value="Test SABnzbd" id="testSABnzbd" class="btn test-button">
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div><!-- /content_use_nzbs //-->
@ -356,7 +358,7 @@
<label for="use_torrents">
<span class="component-title">Search torrents</span>
<span class="component-desc">
<input type="checkbox" name="use_torrents" class="enabler" id="use_torrents" <%= html_checked if sickbeard.USE_TORRENTS == True else '' %>/>
<input type="checkbox" name="use_torrents" class="enabler" id="use_torrents"<%= html_checked if sickbeard.USE_TORRENTS == True else '' %>>
<p>enable torrent search providers</p>
</span>
</label>
@ -381,14 +383,14 @@
<label>
<span class="component-title">Black hole folder location</span>
<span class="component-desc">
<input type="text" name="torrent_dir" id="torrent_dir" value="$sickbeard.TORRENT_DIR" class="form-control input-sm input350" />
<div class="clear-left"><p><b>.torrent</b> files are stored at this location for external software to find and use</p></div>
<input type="text" name="torrent_dir" id="torrent_dir" value="$sickbeard.TORRENT_DIR" class="form-control input-sm input350">
<p class="clear-left note"><b>.torrent</b> files are stored at this location for external software to find and use</p>
</span>
</label>
</div>
<div></div>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div>
</div>
@ -397,12 +399,12 @@
<label>
<span class="component-title" id="host_title">Torrent host:port</span>
<span class="component-desc">
<input type="text" name="torrent_host" id="torrent_host" value="$sickbeard.TORRENT_HOST" class="form-control input-sm input350" />
<div class="clear-left">
<p id="host_desc_torrent">URL to your torrent client (e.g. http://localhost:8000/)</p>
<p id="host_desc_rtorrent" style="display:none"><b>Note:</b> <i>rTorrent</i> client URLs use e.g. scgi://localhost:5000/</p>
<p id="host_desc_deluge" style="display:none">URL to your Deluge WebUI (e.g. http://localhost:8112/)</p>
</div>
<input type="text" name="torrent_host" id="torrent_host" value="$sickbeard.TORRENT_HOST" class="form-control input-sm input350">
<p class="clear-left note">
<span id="host_desc_torrent">URL to your torrent client (e.g. http://localhost:8000/)</span>
<span id="host_desc_rtorrent" style="display:none"><b>Note:</b> <i>rTorrent</i> client URLs use e.g. scgi://localhost:5000/</span>
<span id="host_desc_deluge" style="display:none">URL to your Deluge WebUI (e.g. http://localhost:8112/)</span>
</p>
</span>
</label>
</div>
@ -411,7 +413,7 @@
<label for="torrent_verify_cert">
<span class="component-title">Verify certificate</span>
<span class="component-desc">
<input type="checkbox" name="torrent_verify_cert" class="enabler" id="torrent_verify_cert" <%= html_checked if sickbeard.TORRENT_VERIFY_CERT == True else '' %>/>
<input type="checkbox" name="torrent_verify_cert" class="enabler" id="torrent_verify_cert"<%= html_checked if sickbeard.TORRENT_VERIFY_CERT == True else '' %>>
<p>disable if you get "Deluge: Authentication Error" in your log</p>
</span>
</label>
@ -421,7 +423,7 @@
<label>
<span class="component-title" id="username_title">Client username</span>
<span class="component-desc">
<input type="text" name="torrent_username" id="torrent_username" value="$sickbeard.TORRENT_USERNAME" class="form-control input-sm input200" />
<input type="text" name="torrent_username" id="torrent_username" value="$sickbeard.TORRENT_USERNAME" class="form-control input-sm input200">
<p>(blank for none)</p>
</span>
</label>
@ -431,7 +433,7 @@
<label>
<span class="component-title" id="password_title">Client password</span>
<span class="component-desc">
<input type="password" name="torrent_password" id="torrent_password" value="$sickbeard.TORRENT_PASSWORD" class="form-control input-sm input200" />
<input type="password" name="torrent_password" id="torrent_password" value="#echo '*' * len($sickbeard.TORRENT_PASSWORD)#" class="form-control input-sm input200">
<p>(blank for none)</p>
</span>
</label>
@ -441,9 +443,9 @@
<label>
<span class="component-title">Add label to torrent</span>
<span class="component-desc">
<input type="text" name="torrent_label" id="torrent_label" value="$sickbeard.TORRENT_LABEL" class="form-control input-sm input200" />
<input type="text" name="torrent_label" id="torrent_label" value="$sickbeard.TORRENT_LABEL" class="form-control input-sm input200">
<span id="label_warning_deluge" style="display:none"><p>(blank spaces are not allowed)</p>
<div class="clear-left"><p>note: label plugin must be enabled in Deluge clients</p></div>
<p class="clear-left note">note: label plugin must be enabled in Deluge clients</p>
</span>
</span>
</label>
@ -453,10 +455,9 @@
<label>
<span class="component-title" id="directory_title">Downloaded files location</span>
<span class="component-desc">
<input type="text" name="torrent_path" id="torrent_path" value="$sickbeard.TORRENT_PATH" class="form-control input-sm input350" />
<div class="clear-left"><p>where <span id="torrent_client">the torrent client</span> will save downloaded files (blank for client default)
<input type="text" name="torrent_path" id="torrent_path" value="$sickbeard.TORRENT_PATH" class="form-control input-sm input350">
<p class="clear-left note">where <span id="torrent_client">the torrent client</span> will save downloaded files (blank for client default)
<span id="path_synology"> <b>note:</b> the destination has to be a shared folder for Synology DS</span></p>
</div>
</span>
</label>
</div>
@ -464,7 +465,7 @@
<div class="field-pair" id="torrent_seed_time_option">
<label>
<span class="component-title">Minimum seeding time is</span>
<span class="component-desc"><input type="number" step="0.1" name="torrent_seed_time" id="torrent_seed_time" value="$sickbeard.TORRENT_SEED_TIME" class="form-control input-sm input100" />
<span class="component-desc"><input type="number" step="0.1" name="torrent_seed_time" id="torrent_seed_time" value="$sickbeard.TORRENT_SEED_TIME" class="form-control input-sm input100">
<p>hours. (default:'0' passes blank to client and '-1' passes nothing)</p></span>
</label>
</div>
@ -473,7 +474,7 @@
<label>
<span class="component-title">Start torrent paused</span>
<span class="component-desc">
<input type="checkbox" name="torrent_paused" class="enabler" id="torrent_paused" <%= html_checked if sickbeard.TORRENT_PAUSED == True else '' %>/>
<input type="checkbox" name="torrent_paused" class="enabler" id="torrent_paused"<%= html_checked if sickbeard.TORRENT_PAUSED == True else '' %>>
<p>add .torrent to client but do <b style="font-weight:900">not</b> start downloading</p>
</span>
</label>
@ -483,23 +484,23 @@
<label>
<span class="component-title">Allow high bandwidth</span>
<span class="component-desc">
<input type="checkbox" name="torrent_high_bandwidth" class="enabler" id="torrent_high_bandwidth" <%= html_checked if sickbeard.TORRENT_HIGH_BANDWIDTH == True else '' %>/>
<input type="checkbox" name="torrent_high_bandwidth" class="enabler" id="torrent_high_bandwidth"<%= html_checked if sickbeard.TORRENT_HIGH_BANDWIDTH == True else '' %>>
<p>use high bandwidth allocation if priority is high</p>
</span>
</label>
</div>
<div class="testNotification" id="test_torrent_result">Click below to test</div>
<input class="btn" type="button" value="Test Connection" id="test_torrent" class="btn test-button"/>
<input type="submit" class="btn config_submitter" value="Save Changes" /><br/>
<input class="btn" type="button" value="Test Connection" id="test_torrent" class="btn test-button">
<input type="submit" class="btn config_submitter" value="Save Changes"><br />
</div>
</div><!-- /content_use_torrents //-->
</fieldset>
</div><!-- /component-group3 //-->
<br/>
<br />
<h6 class="pull-right"><b>All non-absolute folder locations are relative to <span class="path">$sickbeard.DATA_DIR</span></b> </h6>
<input type="submit" class="btn pull-left config_submitter button" value="Save Changes" />
<input type="submit" class="btn pull-left config_submitter button" value="Save Changes">
</div><!-- /config-components //-->

View file

@ -1,4 +1,5 @@
#import sickbeard
#import re
#from sickbeard import subtitles, sbdatetime, network_timezones
#import sickbeard.helpers
#from sickbeard.common import *
@ -10,484 +11,587 @@
#set global $title = $show.name
#set global $topmenu = 'home'
#set $exceptions_string = ', '.join($show.exceptions)
#set global $page_body_attr = 'display-show'
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
<script type="text/javascript" src="$sbRoot/js/lib/jquery.bookmarkscroll.js?$sbPID"></script>
<input type="hidden" id="sbRoot" value="$sbRoot" />
<input type="hidden" id="sbRoot" value="$sbRoot">
<script type="text/javascript" src="$sbRoot/js/displayShow.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/plotTooltip.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/sceneExceptionsTooltip.js?$sbPID"></script>
#if $sickbeard.USE_IMDB_INFO
<script type="text/javascript" src="$sbRoot/js/ratingTooltip.js?$sbPID"></script>
#end if
<script type="text/javascript" src="$sbRoot/js/ajaxEpSearch.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/ajaxEpSubtitles.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/lib/jquery.collapser.min.js?$sbPID"></script>
<script type="text/javascript" charset="utf-8">
<!--
\$(document).ready(function(){
#set $fuzzydate = 'airdate'
#if $sickbeard.FUZZY_DATING:
#set $fuzzydate = 'airdate'
#if $sickbeard.FUZZY_DATING
fuzzyMoment({
containerClass : '.${fuzzydate}',
dateHasTime : false,
dateFormat : '${sickbeard.DATE_PRESET}',
timeFormat : '${sickbeard.TIME_PRESET}',
trimZero : #if $sickbeard.TRIM_ZERO then "true" else "false"#
trimZero : #if $sickbeard.TRIM_ZERO then 'true' else 'false'#
});
#end if
#raw
#end if
#raw
$('.addQTip').each(function () {
$(this).css({'cursor':'help', 'text-shadow':'0px 0px 0.5px #666'});
$(this).qtip({
show: {solo:true},
position: {viewport:$(window), my:'left center', adjust:{ y: -10, x: 2 }},
style: {classes:'qtip-rounded qtip-shadow'}
style: {classes:'qtip-rounded qtip-shadow qtip-maxwidth'}
});
});
#end raw
#end raw
#if $sickbeard.USE_IMDB_INFO
\$.fn.generateStars = function() {
return this.each(function(i,e){\$(e).html(\$('<span/>').width(\$(e).text()*12));});
};
\$('.imdbstars').generateStars();
#end if
TVShowList = [${tvshow_id_csv}]
});
//-->
</script>
<div class="displayshow-wrapper reg all">
<div class="background-container">
<div style="" class="background"></div>
</div>
<div class="pull-left form-inline">
Change Show:
<div class="navShow"><img id="prevShow" src="$sbRoot/images/prev.png" alt="&lt;&lt;" title="Prev Show" /></div>
<select id="pickShow" class="form-control form-control-inline input-sm">
#for $curShowList in $sortedShowLists:
#set $curShowType = $curShowList[0]
#set $curShowList = $curShowList[1]
#if len($sortedShowLists) > 1:
<optgroup label="$curShowType">
#end if
#for $curShow in $curShowList:
<option value="$curShow.indexerid" #if $curShow == $show then "selected=\"selected\"" else ""#>$curShow.name</option>
#end for
#if len($sortedShowLists) > 1:
</optgroup>
#end if
#end for
</select>
<div class="navShow"><img id="nextShow" src="$sbRoot/images/next.png" alt="&gt;&gt;" title="Next Show" /></div>
Change show:
#set $displayshowlist = []
#set $cur_sel = 0
#for $curShowList in $sortedShowLists
#set $curShowType = $curShowList[0]
#set $curShowList = $curShowList[1]
#if 1 < len($sortedShowLists)
$displayshowlist.append('\t\t\t<optgroup label="%s">' % $curShowType)
#end if
#for $curShow in $curShowList
#set void = $displayshowlist.append('\t\t\t<option value="%s"%s>%s</option>' % ($curShow.indexerid, ('', ' selected="selected"')[$curShow == $show], $curShow.name))
#if $curShow == $show
#set $cur_sel = len($displayshowlist)
#end if
#end for
#if 1 < len($sortedShowLists)
#set void = $displayshowlist.append('\t\t\t</optgroup>')
#end if
#end for
#set $last_item = len($displayshowlist)
#set $prev_option = $displayshowlist[($cur_sel - 2, $last_item - 1)[1 == $cur_sel]]
#set $next_option = $displayshowlist[($cur_sel, 0)[$last_item == $cur_sel]]
#set $next_match = re.search(r'<opt[^>]+>(.*?)</opt', $prev_option)
#set $prev_match = re.search(r'<opt[^>]+>(.*?)</opt', $next_option)
#set $prev_title = 'Prev show' if not $next_match else 'Prev show, ' + $next_match.group(1)
#set $next_title = 'Next show' if not $prev_match else 'Next show, ' + $prev_match.group(1)
#slurp
<div class="navShow"><img id="prevShow" src="$sbRoot/images/prev.png" alt="&lt;&lt;" title="$prev_title" class="addQTip" /></div>
<select id="pickShow" class="form-control form-control-inline input-sm">
#echo '\n'.join($displayshowlist)#
</select>
<div class="navShow"><img id="nextShow" src="$sbRoot/images/next.png" alt="&gt;&gt;" title="$next_title" class="addQTip" /></div>
</div>
<div class="clearfix"></div>
<div class="clearfix" style="margin-bottom:15px"></div>
<div id="showtitle" data-showname="$show.name">
<h1 class="title" id="scene_exception_$show.indexerid"><span>$show.name</span></h1>
</div>
#if $seasonResults:
##There is a special/season_0?##
#if int($seasonResults[-1]["season"]) == 0:
#set $season_special = 1
#else:
#set $season_special = 0
#end if
#if not $sickbeard.DISPLAY_SHOW_SPECIALS and $season_special:
$seasonResults.pop(-1)
#end if
<span class="h2footer displayspecials pull-right">
#if $season_special:
Display Specials:
#if sickbeard.DISPLAY_SHOW_SPECIALS:
<a class="inner" href="$sbRoot/toggleDisplayShowSpecials/?show=$show.indexerid">Hide</a>
#else:
<a class="inner" href="$sbRoot/toggleDisplayShowSpecials/?show=$show.indexerid">Show</a>
#end if
#end if
</span>
<div class="h2footer pull-right">
<span>
#if (len($seasonResults) > 14):
<select id="seasonJump" class="form-control input-sm" style="position: relative; top: -4px;">
<option value="jump">Jump to Season</option>
#for $seasonNum in $seasonResults:
<option value="#season-$seasonNum["season"]">#if int($seasonNum["season"]) == 0 then "Specials" else "Season " + str($seasonNum["season"])#</option>
#end for
</select>
#else:
Season:
#for $seasonNum in $seasonResults:
#if int($seasonNum["season"]) == 0:
<a href="#season-$seasonNum["season"]">Specials</a>
#else:
<a href="#season-$seasonNum["season"]">${str($seasonNum["season"])}</a>
#end if
#if $seasonNum != $seasonResults[-1]:
<span class="separator">|</span>
#end if
#end for
#end if
</span>
#end if
</div>
<div class="clearfix"></div>
#if $show_message:
#if $show_message
<div class="alert alert-info">
$show_message
</div>
#end if
<div id="container">
<div id="posterCol">
<a href="$sbRoot/showPoster/?show=$show.indexerid&amp;which=poster" rel="dialog" title="View Poster for $show.name"><img src="$sbRoot/showPoster/?show=$show.indexerid&amp;which=poster_thumb" class="tvshowImg" alt=""/></a>
<div class="display-show-container">
<div id="posterCol" class="hidden-xs">
<a href="$sbRoot/showPoster/?show=$show.indexerid&amp;which=poster" rel="dialog" title="View poster for $show.name">
<img src="$sbRoot/showPoster/?show=$show.indexerid&amp;which=poster_thumb" class="tvshowImg" alt="" />
</a>
</div>
<div id="showCol">
<div id="showinfo">
#if 'rating' in $show.imdb_info:
#set $rating_tip = str($show.imdb_info['rating']) + " / 10" + " Stars" + "<br />" + str($show.imdb_info['votes']) + " Votes"
<span class="imdbstars" qtip-content="$rating_tip">$show.imdb_info['rating']</span>
<div id="showCol" class="display-details">
#if int($show.paused)
<div class="paused paused-highlight">
<i class="sgicon-pause paused-outline"></i>
</div>
#end if
#set $_show = $show
#if not $show.imdbid
<span>($show.startyear) - $show.runtime minutes - </span>
#else
#if 'country_codes' in $show.imdb_info:
#for $country in $show.imdb_info['country_codes'].split('|')
<img class="flag" src="$sbRoot/images/flags/${$country}.png" width="16" height="11" />
<div id="details-wrapper">
<div id="details-right">
#if $seasonResults
##There is a special/season_0?##
#set $season_special = (0, 1)[0 == int($seasonResults[-1]['season'])]
#slurp
#if not $sickbeard.DISPLAY_SHOW_SPECIALS and $season_special
$seasonResults.pop(-1)
#end if
<div>
#if $season_special
<span class="details-title">Specials</span>
<span class="details-info">#if sickbeard.DISPLAY_SHOW_SPECIALS#<a href="#season-0">View</a><span style="margin:0 10px">-</span>#end if#<a class="inner" href="$sbRoot/toggleDisplayShowSpecials/?show=$show.indexerid">#echo ('Show', 'Hide')[sickbeard.DISPLAY_SHOW_SPECIALS]#</a></span>
#end if
</div>
#set $many_seasons = 12 < len($seasonResults)
<div class="details-seasons">
<span class="details-title#echo ('', ' combo-seasons')[$many_seasons]#">Season</span>
<span class="details-info">
#if $many_seasons
<select id="seasonJump" class="form-control form-control-inline input-sm">
<option value="jump">Jump to season</option>
#for $seasonNum in $seasonResults
#if 0 == int($seasonNum['season'])
#continue
#end if
<option value="#season-$seasonNum['season']">Season $seasonNum['season']</option>
#end for
</select>
#else:
#for $seasonNum in $seasonResults
#if 0 == int($seasonNum['season'])
#continue
#end if
<a href="#season-$seasonNum['season']">$seasonNum['season']</a>
#end for
#end if
#if 'year' in $show.imdb_info:
<span class="imdb-info">($show.imdb_info['year']) - $show.imdb_info['runtimes'] minutes</span>
</span>
</div>
#end if
</div>
<div id="details-top">
<div id="showtitle" data-showname="$show.name">
<h2 class="title" id="scene_exception_$show.indexerid"><span>$show.name</span></h2>
#set $genres_done = False
#if $sickbeard.USE_IMDB_INFO and 'genres' in $show.imdb_info and '' != $show.imdb_info['genres']
#for $imdbgenre in $show.imdb_info['genres'].split('|')
#set $genres_done = True
<span class="label"><a href="<%= anon_url('http://www.imdb.com/search/title?at=0&genres=', imdbgenre.lower().replace('-','_'),'&amp;sort=moviemeter,asc&amp;title_type=tv_series') %>" target="_blank" title="View other popular $imdbgenre shows on imdb.com">$imdbgenre.replace('Sci-Fi','Science-Fiction')</a></span>
#end for
#end if
#if not $genres_done and $show.genre
#for $genre in $show.genre[1:-1].split('|')
#set $genres_done = True
<span class="label">$genre</span>
#end for#
#end if
#if not $genres_done
<span class="label">No genres</span>
#end if
</div>
</div>
</div>
<div id="details-wrapper">
<div id="details-right">
<div>
<span class="details-title">Indexers</span>
<span class="details-info">
#set $_show = $show
#if $sickbeard.USE_IMDB_INFO and $show.imdbid
<a class="service" href="<%= anon_url('http://www.imdb.com/title/', _show.imdbid) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;" title="Show IMDb info in new tab"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" /></a>
#end if
<a class="service" href="<%= anon_url(sickbeard.indexerApi(_show.indexer).config['show_url'], _show.indexerid) %>" onclick="window.open(this.href, '_blank'); return false;" title="Show $sickbeard.indexerApi($show.indexer).name info in new tab"><img alt="$sickbeard.indexerApi($show.indexer).name" height="16" width="16" src="$sbRoot/images/$sickbeard.indexerApi($show.indexer).config['icon']" /></a>
#if $xem_numbering or $xem_absolute_numbering
<a class="service" href="<%= anon_url('http://thexem.de/search?q=', _show.name) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;" title="Show XEM info in new tab"><img alt="[xem]" height="16" width="16" src="$sbRoot/images/xem.png" /></a>
#end if
</span>
</div>
#set $startyear, $flags, $runtime = (None, False, None)
#if $sickbeard.USE_IMDB_INFO and $show.imdbid
#if 'year' in $show.imdb_info
#set $startyear = $show.imdb_info['year']
#end if
#set $flags = 'country_codes' in $show.imdb_info and '' != $show.imdb_info['country_codes']
#if 'runtimes' in $show.imdb_info
#set $runtime = $show.imdb_info['runtimes']
#end if
<a class="service" href="<%= anon_url('http://www.imdb.com/title/', _show.imdbid) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;" title="http://www.imdb.com/title/$show.imdbid"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" /></a>
#end if
<a class="service" href="<%= anon_url(sickbeard.indexerApi(_show.indexer).config['show_url'], _show.indexerid) %>" onclick="window.open(this.href, '_blank'); return false;" title="$sickbeard.indexerApi($show.indexer).config['show_url']$show.indexerid"><img alt="$sickbeard.indexerApi($show.indexer).name" height="16" width="16" src="$sbRoot/images/$sickbeard.indexerApi($show.indexer).config['icon']" /></a>
#if $xem_numbering or $xem_absolute_numbering:
<a class="service" href="<%= anon_url('http://thexem.de/search?q=', _show.name) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;" title="http://thexem.de/search?q-$show.name"><img alt="[xem]" height="16" width="16" src="$sbRoot/images/xem.png" /></a>
#if None is $startyear and $show.startyear
#set $startyear = $show.startyear
#end if
#if None is $runtime and $show.runtime
#set $runtime = $show.runtime
#end if
#if None is not $startyear or $flags
<div>
<span class="details-title">Premiered</span>
<span class="details-info">
<span class="space-right">#echo ($startyear, 'Unknown')[None is $startyear]#</span>
#if $flags
#for $country in $show.imdb_info['country_codes'].split('|')
<img class="flag space-right" src="$sbRoot/images/flags/${$country}.png" width="16" height="11" />
#end for
#end if
</span>
</div>
#end if
#if $show.airs
#set $showairs = '%s%s' % ($show.airs.replace('y', 'y,'),
('', ' <span class="red-text" style="font-weight:bold">(invalid timeformat)</span>')[not $network_timezones.test_timeformat($show.airs)])
<div>
<span class="details-title">Airs</span>
<span class="details-info">$showairs</span>
</div>
#end if
#if $show.network
<div>
<span class="details-title">Network</span>
<span class="details-info">$show.network</span>
</div>
#end if
#if None is not $runtime
<div>
<span class="details-title">Runtime</span>
<span class="details-info">$runtime minutes</span>
</div>
#end if
#if '' != $show.status
<div>
<span class="details-title">Status</span>
<span class="details-info">$show.status</span>
</div>
#end if
#if $sickbeard.USE_IMDB_INFO and 'rating' in $show.imdb_info
<div>
<span class="details-title">IMDb rating</span>
<span class="details-info">
#if '' != $show.imdb_info['votes']
#set $rating_tip = '%s of 10 stars<br />%s votes' % (str($show.imdb_info['rating']), str($show.imdb_info['votes']))
<span class="imdbstars" qtip-content="$rating_tip">$show.imdb_info['rating']</span>
#else
<span>No votes available</span>
#end if
</span>
</div>
#end if
#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality))
#if $show.quality in $qualityPresets
<div>
<span class="details-title">Quality</span>
<span class="details-info">
<span class="quality $qualityPresetStrings[$show.quality]">$qualityPresetStrings[$show.quality]</span>
</span>
</div>
#else:
#if $anyQualities
<div>
<span class="details-title">Initial</span>
<span class="details-info">
#echo ', '.join([$Quality.qualityStrings[$x] for $x in sorted($anyQualities)])#
</span>
</div>
#end if
#if $bestQualities
<div>
<span class="details-title">Replace with</span>
<span class="details-info">
#echo ', '.join([$Quality.qualityStrings[$x] for $x in sorted($bestQualities)])#
</span>
</div>
#end if
#end if
</div>
<div id="details-left">
<div class="details-plot#echo ('', ' no-plot')['' == $show.overview]#">
#echo ('No plot overview available', $show.overview)['' != $show.overview]#
</div>
<div id="details-bottom">
<span class="label addQTip" title="Info language, $show.lang"><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="" style="margin-top:-1px" /></span>
<span class="label addQTip" title="Location#echo (' no longer exists" style="background-color:#8f1515"', '"')[$showLoc[1]]#>$showLoc[0]</span>
<span class="label addQTip" title="Size">$sickbeard.helpers.human(sickbeard.helpers.get_size($showLoc[0]))</span>
#set $filecount = sum([$c for $k, $c in $epCounts['videos'].items()])
<span class="label addQTip" title="Videos">$filecount file$sickbeard.helpers.maybe_plural($filecount)</span>
#if $show.paused
<span class="label label-paused">Paused</span>
#end if
#if ($anyQualities + $bestQualities) and int($show.archive_firstmatch)
<span class="label">Archive first match</span>
#end if
#if $show.exceptions
<span class="label addQTip" title="$exceptions_string.replace(', ', '<br />')">Scene names</span>
#end if
#if $show.rls_ignore_words
<span class="label addQTip" title="#echo $show.rls_ignore_words.replace(',', '<br />')#">Ignored words</span>
#end if
#if $show.rls_require_words
<span class="label addQTip" title="#echo $show.rls_require_words.replace(',', '<br />')#">Required words</span>
#end if
#if $show.flatten_folders or $sickbeard.NAMING_FORCE_FOLDERS
<span class="label">Flat folders</span>
#end if
#if int($show.air_by_date)
<span class="label">Air by date</span>
#end if
#if int($show.dvdorder)
<span class="label">DVD order</span>
#end if
#if int($show.scene)
<span class="label">Scene numbering</span>
#end if
#if $sickbeard.USE_SUBTITLES and int($show.subtitles)
<span class="label">Subtitles</span>
#end if
#if int($show.is_sports)
<span class="label">Sports</span>
#end if
#if int($show.is_anime)
<span class="label">Anime</span>
#end if
#if $bwl and $bwl.whitelist
<span class="label addQTip" title="#echo ', '.join($bwl.whitelist).replace(',', '<br />')#">Wanted group$sickbeard.helpers.maybe_plural(len($bwl.whitelist))</span>
#end if
#if $bwl and $bwl.blacklist
<span class="label addQTip" title="#echo ', '.join($bwl.blacklist).replace(',', '<br />')#">Unwanted group$sickbeard.helpers.maybe_plural(len($bwl.blacklist))</span>
#end if
</div>
</div>
</div>
<div id="tags">
<ul class="tags">
#if not $show.imdbid
#if $show.genre:
#for $genre in $show.genre[1:-1].split('|')
<a href="<%= anon_url('http://trakt.tv/shows/popular/', genre.lower()) %>" target="_blank" title="View other popular $genre shows on trakt.tv."><li>$genre</li></a>
#end for
#end if
#end if
#if 'year' in $show.imdb_info:
#for $imdbgenre in $show.imdb_info['genres'].replace('Sci-Fi','Science-Fiction').split('|')
<a href="<%= anon_url('http://trakt.tv/shows/popular/', imdbgenre.lower()) %>" target="_blank" title="View other popular $imdbgenre shows on trakt.tv."><li>$imdbgenre</li></a>
#end for
#end if
</ul>
</div>
<div id="summary">
<table class="summaryTable pull-left">
#if $show.network and $show.airs:
<tr><td class="showLegend grey-text">Originally airs</td><td>$show.airs #if not $network_timezones.test_timeformat($show.airs) then ' <font color="#FF0000"><b>(invalid Timeformat)</b></font> ' else ''# on $show.network</td></tr>
#else if $show.network:
<tr><td class="showLegend grey-text">Originally airs</td><td>$show.network</td></tr>
#else if $show.airs:
<tr><td class="showLegend grey-text">Originally airs</td><td>$show.airs #if not $network_timezones.test_timeformat($show.airs) then ' <font color="#FF0000"><b>(invalid Timeformat)</b></font> ' else ''#</td></tr>
#end if
<tr><td class="showLegend grey-text">Status</td><td>$show.status</td></tr>
#if $showLoc[1]:
<tr><td class="showLegend grey-text">Location</td><td>$showLoc[0]</td></tr>
#else:
<tr><td class="showLegend grey-text"><span style="color: red;">Location</span></td><td><span style="color: red;">$showLoc[0]</span> (dir is missing)</td></tr>
#end if
#set $anyQualities, $bestQualities = $Quality.splitQuality(int($show.quality))
<tr><td class="showLegend grey-text">Quality</td><td>
#if $show.quality in $qualityPresets:
<span class="quality $qualityPresetStrings[$show.quality]">$qualityPresetStrings[$show.quality]</span>
#else:
#if $anyQualities:
<i class="grey-text">Initial ...</i> <%= ', '.join([Quality.qualityStrings[x] for x in sorted(anyQualities)])%> #if $bestQualities then " </br> " else ""#
#end if
#if $bestQualities:
<i class="grey-text">Replace with ...</i> <%= ', '.join([Quality.qualityStrings[x] for x in sorted(bestQualities)])%>
#end if
#end if
<tr><td class="showLegend grey-text">Scene name</td><td>#if $show.exceptions then $exceptions_string else $show.name#</td></tr>
#if $show.rls_ignore_words:
<tr><td class="showLegend grey-text">Ignore with any of</td><td>#echo $show.rls_ignore_words#</td></tr>
#end if
#if $show.rls_require_words:
<tr><td class="showLegend grey-text">Require one of</td><td>#echo $show.rls_require_words#</td></tr>
#end if
#if $bwl and $bwl.whitelist:
<tr><td class="showLegend grey-text">Whitelist group#if len($bwl.whitelist)>1 then 's' else ''#</td>
<td>#echo ', '.join($bwl.whitelist)#</td>
</tr>
#end if
#if $bwl and $bwl.blacklist:
<tr><td class="showLegend grey-text">Blacklist group#if len($bwl.blacklist)>1 then 's' else ''#</td>
<td>#echo ', '.join($bwl.blacklist)#</td>
</tr>
#end if
<tr><td class="showLegend grey-text">Size</td><td>$sickbeard.helpers.human(sickbeard.helpers.get_size($showLoc[0]))</td></tr>
</table>
<table class="options-on-right">
<tr><td class="showLegendRight grey-text">Paused</td><td><img src="$sbRoot/images/#if int($show.paused) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
#if $anyQualities + $bestQualities
<tr><td class="showLegendRight grey-text">Archive on first match</td><td><img src="$sbRoot/images/#if int($show.archive_firstmatch) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
#end if
<tr><td class="showLegendRight grey-text">Flat folder structure</td><td><img src="$sbRoot/images/#if $show.flatten_folders == 1 or $sickbeard.NAMING_FORCE_FOLDERS then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
<tr><td class="showLegendRight grey-text">Air by date naming</td><td><img src="$sbRoot/images/#if int($show.air_by_date) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
<tr><td class="showLegendRight grey-text">Use DVD order</td><td><img src="$sbRoot/images/#if int($show.dvdorder) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
<tr><td class="showLegendRight grey-text">Scene numbering</td><td><img src="$sbRoot/images/#if int($show.scene) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
#if $sickbeard.USE_SUBTITLES
<tr><td class="showLegendRight grey-text">Subtitles</td><td><img src="$sbRoot/images/#if int($show.subtitles) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
#end if
<tr><td class="showLegendRight grey-text">Show is sports</td><td><img src="$sbRoot/images/#if int($show.is_sports) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
<tr><td class="showLegendRight grey-text">Show is anime</td><td><img src="$sbRoot/images/#if int($show.is_anime) == 1 then 'yes16.png" title="Yes" alt="Yes' else 'no16.png" title="No" alt="No'#" width="16" height="16" /></td></tr>
<tr><td class="showLegendRight grey-text">Info language</td><td><img src="$sbRoot/images/flags/${show.lang}.png" width="16" height="11" alt="$show.lang" title="$show.lang" /></td></tr>
</table>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="pull-left" style="padding-bottom: 10px;">
Change selected episodes to
<div class="pull-left">
<p style="margin-bottom:5px">Change selected episodes to</p>
<select id="statusSelect" class="form-control form-control-inline input-sm">
#for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED, $FAILED] + sorted($Quality.DOWNLOADED):
#if $curStatus == $DOWNLOADED:
#continue
#end if
#for $curStatus in [$WANTED, $SKIPPED, $ARCHIVED, $IGNORED, $FAILED] + sorted($Quality.DOWNLOADED)
#if $DOWNLOADED == $curStatus
#continue
#end if
<option value="$curStatus">$statusStrings[$curStatus]</option>
#end for
#end for
</select>
<input type="hidden" id="showID" value="$show.indexerid" />
<input type="hidden" id="indexer" value="$show.indexer" />
<input class="btn btn-inline" type="button" id="changeStatus" value="Go" />
<input type="hidden" id="showID" value="$show.indexerid">
<input type="hidden" id="indexer" value="$show.indexer">
<input class="btn btn-inline" type="button" id="changeStatus" value="Go">
</div>
<div class="pull-right clearfix" id="checkboxControls">
<div style="padding-bottom: 5px;">
<label for="wanted"><span class="wanted"><input type="checkbox" id="wanted" checked="checked" /> Wanted: <b>$epCounts[$Overview.WANTED]</b></span></label>
<label for="qual"><span class="qual"><input type="checkbox" id="qual" checked="checked" /> Low Quality: <b>$epCounts[$Overview.QUAL]</b></span></label>
<label for="good"><span class="good"><input type="checkbox" id="good" checked="checked" /> Downloaded: <b>$epCounts[$Overview.GOOD]</b></span></label>
<label for="skipped"><span class="skipped"><input type="checkbox" id="skipped" checked="checked" /> Skipped: <b>$epCounts[$Overview.SKIPPED]</b></span></label>
<label for="snatched"><span class="snatched"><input type="checkbox" id="snatched" checked="checked" /> Snatched: <b>$epCounts[$Overview.SNATCHED]</b></span></label>
<div style="padding-bottom:5px">
<label for="wanted"><span class="wanted"><input type="checkbox" id="wanted" checked="checked"> Wanted: <b>$epCounts[$Overview.WANTED]</b></span></label>
<label for="qual"><span class="qual"><input type="checkbox" id="qual" checked="checked"> Low quality: <b>$epCounts[$Overview.QUAL]</b></span></label>
<label for="good"><span class="good"><input type="checkbox" id="good" checked="checked"> Downloaded: <b>$epCounts[$Overview.GOOD]</b></span></label>
<label for="skipped"><span class="skipped"><input type="checkbox" id="skipped" checked="checked"> Skipped: <b>$epCounts[$Overview.SKIPPED]</b></span></label>
<label for="snatched"><span class="snatched"><input type="checkbox" id="snatched" checked="checked"> Snatched: <b>$epCounts[$Overview.SNATCHED]</b></span></label>
</div>
<div class="pull-right" >
<button class="btn btn-xs seriesCheck">Select Filtered Episodes</button>
<button class="btn btn-xs clearAll">Clear All</button>
<button class="btn btn-xs seriesCheck">Select filtered episodes</button>
<button class="btn btn-xs clearAll">Clear all</button>
</div>
</div>
<br />
<table class="sickbeardTable display_show" cellspacing="0" border="0" cellpadding="0">
<div class="clearfix"></div>
#set $curSeason = -1
#set $odd = 0
#set $scene, $scene_anime = (False, False)
#if not $show.air_by_date and not $show.is_sports and not $show.is_anime and $show.is_scene
#set $scene = True
#elif not $show.air_by_date and not $show.is_sports and $show.is_anime and $show.is_scene
#set $scene_anime = True
#end if
#slurp
#if 0 == len($sqlResults)
<div style="margin-top:50px"><h3>Episodes no longer exist for this show at the associated indexer</h3></div>
#else:
#for $epResult in $sqlResults
#set $epStr = '%sx%s' % ($epResult['season'], $epResult['episode'])
#if not $epStr in $epCats or (0 == int($epResult['season']) and not $sickbeard.DISPLAY_SHOW_SPECIALS)
#continue
#end if
#slurp
#if $curSeason != int($epResult['season'])
#if 0 <= $curSeason
</tbody>
</table>
#end if
#for $epResult in $sqlResults:
#set $epStr = str($epResult["season"]) + "x" + str($epResult["episode"])
#if not $epStr in $epCats:
#continue
#end if
<table class="sickbeardTable" cellspacing="0" border="0" cellpadding="0">
<tr id="season-$epResult['season']">
<th class="row-seasonheader" colspan="13">
#if not $sickbeard.DISPLAY_SHOW_SPECIALS and int($epResult["season"]) == 0:
#continue
#end if
#set $scene = False
#set $scene_anime = False
#if not $show.air_by_date and not $show.is_sports and not $show.is_anime and $show.is_scene:
#set $scene = True
#elif not $show.air_by_date and not $show.is_sports and $show.is_anime and $show.is_scene:
#set $scene_anime = True
#end if
#set ($dfltSeas, $dfltEpis, $dfltAbsolute) = (0, 0, 0)
#if (epResult["season"], epResult["episode"]) in $xem_numbering:
#set ($dfltSeas, $dfltEpis) = $xem_numbering[(epResult["season"], epResult["episode"])]
#end if
#if epResult["absolute_number"] in $xem_absolute_numbering:
#set $dfltAbsolute = $xem_absolute_numbering[epResult["absolute_number"]]
#end if
#if epResult["absolute_number"] in $scene_absolute_numbering:
#set $scAbsolute = $scene_absolute_numbering[epResult["absolute_number"]]
#set $dfltAbsNumbering = False
#else
#set $scAbsolute = $dfltAbsolute
#set $dfltAbsNumbering = True
#end if
#if (epResult["season"], epResult["episode"]) in $scene_numbering:
#set ($scSeas, $scEpis) = $scene_numbering[(epResult["season"], epResult["episode"])]
#set $dfltEpNumbering = False
#else
#set ($scSeas, $scEpis) = ($dfltSeas, $dfltEpis)
#set $dfltEpNumbering = True
#end if
#if int($epResult["season"]) != $curSeason:
<tr id="season-$epResult['season']">
<th class="row-seasonheader" colspan="13" style="width: auto;"><h3><a name="season-$epResult["season"]"></a>#if int($epResult["season"]) == 0 then "Specials" else "Season " + str($epResult["season"])#</h3></th>
</tr>
<tr id="season-$epResult["season"]-cols" class="seasoncols">
<th class="col-checkbox"><input type="checkbox" class="seasonCheck" id="$epResult["season"]" /></th>
<th class="col-metadata">NFO</th>
<th class="col-metadata">TBN</th>
<th class="col-ep">Episode</th>
#if $show.is_anime:
<th class="col-ep">Absolute</th>
#end if
#if $scene:
<th class="col-ep">Scene</th>
#end if
#if $scene_anime:
<th class="col-ep">Scene Absolute</th>
#end if
<th class="col-name">Name</th>
<th class="col-airdate">Airdate</th>
#if $sickbeard.USE_SUBTITLES and $show.subtitles:
<th class="col-subtitles">Subtitles</th>
#end if
<th class="col-status">Status</th>
<th class="col-search">Search</th>
</tr>
#set $curSeason = int($epResult["season"])
#end if
#set $epLoc = $epResult["location"]
<tr class="$Overview.overviewStrings[$epCats[$epStr]] season-$curSeason seasonstyle">
<td class="col-checkbox">
#if int($epResult["status"]) != $UNAIRED
<input type="checkbox" class="epCheck" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" />
#end if
</td>
<td align="center"><img src="$sbRoot/images/#if $epResult["hasnfo"] == 1 then "nfo.gif\" alt=\"Y" else "nfo-no.gif\" alt=\"N"#" width="23" height="11" /></td>
<td align="center"><img src="$sbRoot/images/#if $epResult["hastbn"] == 1 then "tbn.gif\" alt=\"Y" else "tbn-no.gif\" alt=\"N"#" width="23" height="11" /></td>
<td align="center">
#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower()):
#set $epLoc = $epLoc[len($show._location)+1:]
#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location):
#set $epLoc = $epLoc
#end if
#if $epLoc != "" and $epLoc != None:
<span title="$epLoc" class="addQTip">$epResult["episode"]</span>
#else
$epResult["episode"]
#end if
</td>
#if $show.is_anime:
<td align="center">$epResult["absolute_number"]</td>
#end if
#if $scene:
<td align="center">
<input type="text" placeholder="<%=str(dfltSeas) + 'x' + str(dfltEpis)%>" size="6" maxlength="8"
class="sceneSeasonXEpisode form-control input-scene" data-for-season="$epResult["season"]" data-for-episode="$epResult["episode"]"
id="sceneSeasonXEpisode_$show.indexerid<%="_"+str(epResult["season"])+"_"+str(epResult["episode"])%>"
title="Change the value here if scene numbering differs from the indexer episode numbering"
#if $dfltEpNumbering:
value=""
#else
value="<%=str(scSeas) + 'x' + str(scEpis)%>"
#end if
style="padding: 0; text-align: center; max-width: 60px;" />
</td>
#elif $scene_anime:
<td align="center">
<input type="text" placeholder="<%=str(dfltAbsolute)%>" size="6" maxlength="8"
class="sceneAbsolute form-control input-scene" data-for-absolute="$epResult["absolute_number"]"
id="sceneAbsolute_$show.indexerid<%="_"+str(epResult["absolute_number"])%>"
title="Change the value here if scene absolute numbering differs from the indexer absolute numbering"
#if $dfltAbsNumbering:
value=""
#else
value="<%=str(scAbsolute)%>"
#end if
style="padding: 0; text-align: center; max-width: 60px;" />
</td>
#end if
<td class="col-name">
#if $epResult["description"] != "" and $epResult["description"] != None:
<img src="$sbRoot/images/info32.png" width="16" height="16" class="plotInfo" alt="" id="plot_info_$show.indexerid<%="_" + str(epResult["season"]) + "_" + str(epResult["episode"])%>" />
#else:
<img src="$sbRoot/images/info32.png" width="16" height="16" class="plotInfoNone" alt="" />
#end if
$epResult["name"]
</td>
<td class="col-airdate">
<span class="${fuzzydate}">#if int($epResult['airdate']) == 1 then 'never' else $sbdatetime.sbdatetime.sbfdate($sbdatetime.sbdatetime.convert_to_setting($network_timezones.parse_date_time($epResult['airdate'],$show.airs,$show.network)))#</span>
</td>
#if $sickbeard.USE_SUBTITLES and $show.subtitles:
<td class="col-subtitles" align="center">
#if $epResult["subtitles"]:
#for $sub_lang in subliminal.language.language_list($epResult["subtitles"].split(',')):
#if sub_lang.alpha2 != ""
<img src="$sbRoot/images/flags/${sub_lang.alpha2}.png" width="16" height="11" alt="${sub_lang}" />
#end if
#end for
#end if
</td>
#end if
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult["status"]))
#if $curQuality != Quality.NONE:
<td class="col-status">$statusStrings[$curStatus] <span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td>
#else:
<td class="col-status">$statusStrings[$curStatus]</td>
#end if
<td class="col-search">
#if int($epResult["season"]) != 0:
#if ( int($epResult["status"]) in $Quality.SNATCHED or int($epResult["status"]) in $Quality.DOWNLOADED ) and $sickbeard.USE_FAILED_DOWNLOADS:
<a class="epRetry" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" href="retryEpisode?show=$show.indexerid&amp;season=$epResult["season"]&amp;episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" height="16" alt="retry" title="Retry Download" /></a>
#else:
<a class="epSearch" id="<%=str(epResult["season"])+'x'+str(epResult["episode"])%>" name="<%=str(epResult["season"]) +"x"+str(epResult["episode"]) %>" href="searchEpisode?show=$show.indexerid&amp;season=$epResult["season"]&amp;episode=$epResult["episode"]"><img src="$sbRoot/images/search16.png" width="16" height="16" alt="search" title="Manual Search" /></a>
#end if
#end if
#if $sickbeard.USE_SUBTITLES and $show.subtitles and len(set(str($epResult["subtitles"]).split(',')).intersection(set($subtitles.wantedLanguages()))) < len($subtitles.wantedLanguages()) and $epResult["location"]
<a class="epSubtitlesSearch" href="searchEpisodeSubtitles?show=$show.indexerid&amp;season=$epResult["season"]&amp;episode=$epResult["episode"]"><img src="$sbRoot/images/closed_captioning.png" height="16" alt="search subtitles" title="Search Subtitles" /></a>
#end if
</td>
<button id="showseason-$epResult['season']" type="button" class="btn btn-default pull-right#echo '%s%s' % (('', ' display-season')[int($epResult['season']) in $display_seasons], ('', ' latest-season')[$latest_season == int($epResult['season'])])#" data-toggle="collapse" data-target="#collapseSeason-$epResult['season']">Show episodes<span class="sgicon-arrowdown" style="margin-left:4px"></span></button>
#set $videos = 'none' if $epResult['season'] not in $epCounts['videos'] else $epCounts['videos'][$epResult['season']]
#set $archived = False if $epResult['season'] not in $epCounts['archived'] else $epCounts['archived'][$epResult['season']]
<h3><a name="season-$epResult['season']"></a>#if 0 == int($epResult['season']) then 'Specials' else 'Season ' + str($epResult['season'])
<span class="season-status"><b>[</b> <span class="footerhighlight">$videos</span> / <span class="footerhighlight">$epCounts['totals'][$epResult['season']]</span><span class="archived-count">#echo ('', '&nbsp;with <span class="footerhighlight">%s</span> archived' % $archived)[0 < $archived]#</span> <b>]</b></span>
</h3>
</th>
</tr>
#end for
</table>
<tbody id="collapseSeason-$epResult['season']" class="collapse#echo '%s%s' % (('', ' display-season')[int($epResult['season']) in $display_seasons], ('', ' latest-season')[$latest_season == int($epResult['season'])])#">
<tr id="season-$epResult['season']-cols" class="seasoncols">
<th class="col-checkbox"><input type="checkbox" class="seasonCheck" id="$epResult['season']"></th>
<th class="col-metadata">NFO</th>
<th class="col-metadata">TBN</th>
<th class="col-ep">Episode</th>
#if $show.is_anime
<th class="col-ep">Absolute</th>
#end if
#if $scene
<th class="col-ep">Scene</th>
#end if
#if $scene_anime
<th class="col-ep">Scene absolute</th>
#end if
<th class="col-name">Name</th>
<th class="col-airdate">Airdate</th>
#if $sickbeard.USE_SUBTITLES and $show.subtitles
<th class="col-subtitles">Subtitles</th>
#end if
<th class="col-status">Status</th>
<th class="col-search">Search</th>
</tr>
#set $curSeason = int($epResult['season'])
#end if
#set $epLoc = $epResult['location']
#set never_aired = 0 < $curSeason and 1 == int($epResult['airdate'])
<tr class="#echo ($Overview.overviewStrings[$epCats[$epStr]], 'airdate-never')[$never_aired]##echo ('', ' archived')[ARCHIVED == int($epResult['status'])]# season-$curSeason seasonstyle">
<td class="col-checkbox">
#if $UNAIRED != int($epResult['status']) and not $never_aired
<input type="checkbox" class="epCheck" id="#echo $epStr#" name="#echo $epStr#">
#end if
</td>
<td align="center"><img src="$sbRoot/images/#if int($epResult['hasnfo']) then 'nfo.gif" alt="Yes" title="Yes' else 'nfo-no.gif" alt="No" title="No'#" width="23" height="11" /></td>
<td align="center"><img src="$sbRoot/images/#if int($epResult['hastbn']) then 'tbn.gif" alt="Yes" title="Yes' else 'tbn-no.gif" alt="No" title="No'#" width="23" height="11" /></td>
<td align="center">
#if $epLoc and $show._location and $epLoc.lower().startswith($show._location.lower())
#set $epLoc = $epLoc[len($show._location)+1:]
#elif $epLoc and (not $epLoc.lower().startswith($show._location.lower()) or not $show._location)
#set $epLoc = $epLoc
#end if
#if '' != $epLoc and None != $epLoc
<span title="$epLoc - <strong>$sickbeard.helpers.human($epResult['file_size'])</strong>" class="addQTip">$epResult["episode"]</span>
#else
$epResult['episode']
#end if
</td>
#if $show.is_anime
<td align="center">$epResult['absolute_number']</td>
#end if
#if $scene
#set $dfltSeas, $dfltEpis = (0, 0) if ($epResult['season'], $epResult['episode']) not in $xem_numbering else $xem_numbering[($epResult['season'], $epResult['episode'])]
<td align="center">
<input type="text" placeholder="#echo '%sx%s' % ($dfltSeas, $dfltEpis)#" size="6" maxlength="8"
class="sceneSeasonXEpisode form-control input-scene" data-for-season="$epResult['season']" data-for-episode="$epResult['episode']"
id="#echo 'sceneSeasonXEpisode_%s_%s_%s' % ($show.indexerid, $epResult['season'], $epResult['episode'])#"
title="Change the value here if scene numbering differs from the indexer episode numbering"
#if ($epResult['season'], $epResult['episode']) in $scene_numbering
#set $scSeas, $scEpis = $scene_numbering[($epResult['season'], $epResult['episode'])]
value="#echo '%sx%s' % ($scSeas, $scEpis)#"
#else
value=""
#end if
style="padding:0; text-align:center; max-width:60px">
</td>
#elif $scene_anime
#set $dfltAbsolute = 0 if $epResult['absolute_number'] not in $xem_absolute_numbering else $xem_absolute_numbering[$epResult['absolute_number']]
<td align="center">
<input type="text" placeholder="$dfltAbsolute" size="6" maxlength="8"
class="sceneAbsolute form-control input-scene" data-for-absolute="$epResult['absolute_number']"
id="#echo 'sceneAbsolute_%s_%s' % ($show.indexerid, $epResult['absolute_number'])#"
title="Change the value here if scene absolute numbering differs from the indexer absolute numbering"
#if $epResult['absolute_number'] in $scene_absolute_numbering
value="$scene_absolute_numbering[$epResult['absolute_number']]"
#else
value=""
#end if
style="padding:0; text-align:center; max-width:60px" />
</td>
#end if
<td class="col-name">
<img src="$sbRoot/images/info32.png" width="16" height="16" alt="" class="plotInfo#echo '%s" />' %\
('None', ('" id="plot_info_%s_%s_%s' % ($show.indexerid, $epResult['season'], $epResult['episode'])))[None is not $epResult['description'] and '' != $epResult['description']]#
$epResult['name']
</td>
<td class="col-airdate">
<span class="${fuzzydate}">#if 1 == int($epResult['airdate']) then 'never' else $sbdatetime.sbdatetime.sbfdate($sbdatetime.sbdatetime.convert_to_setting($network_timezones.parse_date_time($epResult['airdate'], $show.airs, $show.network)))#</span>
</td>
#if $sickbeard.USE_SUBTITLES and $show.subtitles
<td class="col-subtitles" align="center">
#if $epResult['subtitles']
#for $sub_lang in subliminal.language.language_list($epResult['subtitles'].split(','))
#if '' != sub_lang.alpha2
<img src="$sbRoot/images/flags/${sub_lang.alpha2}.png" width="16" height="11" alt="${sub_lang}" />
#end if
#end for
#end if
</td>
#end if
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($epResult['status']))
#if Quality.NONE != $curQuality
<td class="col-status">$statusStrings[$curStatus] <span class="quality $Quality.qualityStrings[$curQuality].replace('720p','HD720p').replace('1080p','HD1080p').replace('RawHD TV', 'RawHD').replace('HD TV', 'HD720p')">$Quality.qualityStrings[$curQuality]</span></td>
#else:
<td class="col-status">$statusStrings[$curStatus]</td>
#end if
<td class="col-search">
#if 0 != int($epResult['season'])
#if (int($epResult['status']) in $Quality.SNATCHED or int($epResult['status']) in $Quality.DOWNLOADED) and $sickbeard.USE_FAILED_DOWNLOADS
<a class="epRetry" id="#echo $epStr#" name="#echo $epStr#" href="retryEpisode?show=$show.indexerid&amp;season=$epResult['season']&amp;episode=$epResult['episode']"><img src="$sbRoot/images/search16.png" height="16" alt="retry" title="Retry download" /></a>
#else:
<a class="epSearch" id="#echo $epStr#" name="#echo $epStr#" href="searchEpisode?show=$show.indexerid&amp;season=$epResult['season']&amp;episode=$epResult['episode']"><img src="$sbRoot/images/search16.png" width="16" height="16" alt="search" title="Manual search" /></a>
#end if
#end if
#if $sickbeard.USE_SUBTITLES and $show.subtitles and len(set(str($epResult['subtitles']).split(',')).intersection(set($subtitles.wantedLanguages()))) < len($subtitles.wantedLanguages()) and $epResult['location']
<a class="epSubtitlesSearch" href="searchEpisodeSubtitles?show=$show.indexerid&amp;season=$epResult['season']&amp;episode=$epResult['episode']"><img src="$sbRoot/images/closed_captioning.png" height="16" alt="search subtitles" title="Search subtitles" /></a>
#end if
</td>
</tr>
#end for
</tbody>
</table>
#end if
<script type="text/javascript" charset="utf-8">
#raw
$(document).ready(function(){
$('.details-plot').collapser({
mode: 'lines',
truncate: 10,
showText: '<span class="pull-right moreless"><i class="sgicon-arrowdown" style="margin-right:2px"></i>more</span>',
hideText: '<span class="pull-right moreless"><i class="sgicon-arrowup" style="margin-right:2px"></i>less</span>',
showClass: 'show-class'
});
$('button[data-target*="collapseSeason-"]').each(function(k,v){
var tbl = $($(this).attr('data-target')),
btn = $('#' + $(this).attr('id'));
tbl.on('hide.bs.collapse', function () { btn.html('Show episodes<span class="sgicon-arrowdown" style="margin-left:4px"></span>'); })
tbl.on('show.bs.collapse', function () { btn.html('Hide episodes<span class="sgicon-arrowup" style="margin-left:4px"></span>'); })
});
#end raw
});
</script>
</div>
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -63,7 +63,7 @@
<form action="editShow" method="post" id="addShowForm">
<input type="hidden" name="show" value="$show.indexerid">
<div id="editShow" class="stepDiv">
<div id="editShow" class="stepDiv linefix">
<div class="field-pair">
<label for="paused">
@ -96,21 +96,21 @@
<div class="field-pair">
<label for="SceneName">
<span class="component-title input">Scene exception</span>
<span class="component-title">Scene exception</span>
<span class="component-desc">
<input type="text" id="SceneName" class="form-control form-control-inline input-sm input200">
<input class="btn btn-inline" type="button" value="Add" id="addSceneName">
<p class="clear-left">add alternative release names found on search providers for <b class="boldest grey-text">$show.name</b></p>
<p class="clear-left note">add alternative release names found on search providers for <b class="boldest grey-text">$show.name</b></p>
</span>
<span class="component-desc">
<div id="SceneException">
<h4>Exceptions list (multi-selectable)</h4>
<h4 class="grey-text">Exceptions list (multi-selectable)</h4>
<select id="exceptions_list" name="exceptions_list" multiple="multiple" class="input200" style="min-height:90px; float:left" >
#for $cur_exception in $show.exceptions:
<option value="$cur_exception">$cur_exception</option>
#end for
</select>
<span><p>this list overrides the original name<br />to search, it doesn't append to it</p></span>
<span><p class="note">this list overrides the original name<br />to search, it doesn't append to it</p></span>
<div>
<input id="removeSceneName" value="Remove" class="btn pull-left" type="button" style="margin-top: 10px;"/>
</div>
@ -122,29 +122,29 @@
<div class="field-pair">
<label for="rls_ignore_words">
<span class="component-title input">Ignore result with any word</span>
<span class="component-title">Ignore result with any word</span>
<span class="component-desc">
<input type="text" name="rls_ignore_words" id="rls_ignore_words" value="$show.rls_ignore_words" class="form-control form-control-inline input-sm input350">
<p>e.g. [word1,word2, ... ,word_n]</p>
<span><p>ignore search result <em class="grey-text">if its title contains any</em> of these comma seperated words</p></span>
<p class="note">ignore search result <em class="grey-text">if its title contains any</em> of these comma seperated words</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="rls_require_words">
<span class="component-title input">Require at least one word</span>
<span class="component-title">Require at least one word</span>
<span class="component-desc">
<input type="text" name="rls_require_words" id="rls_require_words" value="$show.rls_require_words" class="form-control form-control-inline input-sm input350">
<p>e.g. [word1,word2, ... ,word_n]</p>
<span><p>ignore search result <em class="grey-text">unless its title contains one</em> of these comma seperated words</p></span>
<p class="note">ignore search result <em class="grey-text">unless its title contains one</em> of these comma seperated words</p>
</span>
</label>
</div>
<div class="field-pair">
<label for="location">
<span class="component-title input">Location for files</span>
<span class="component-title">Location for files</span>
<span class="component-desc">
<input type="text" name="location" id="location" value="$show._location" class="form-control form-control-inline input-sm input350">
</span>
@ -193,7 +193,7 @@
<div class="field-pair" style="margin-bottom:10px">
<label for="indexerLangSelectEdit">
<span class="component-title input">Info language</span>
<span class="component-title">Info language</span>
<span class="component-desc">
<select name="indexerLang" id="indexerLangSelectEdit" class="form-control form-control-inline input-sm"></select>
<span>attempt to fetch show data and episode filenames in this language</span>

View file

@ -191,7 +191,7 @@
<span class="listing-key listing-toofar">Distant</span>
#end if
<a class="btn btn-inline forceBacklog" href="webcal://$sbHost:$sbHttpPort/calendar">
<i class="icon-calendar icon-white"></i>Subscribe</a>
<i class="sgicon-rss"></i> Subscribe</a>
</div>
<br>
@ -366,7 +366,7 @@
</td>
<td align="center" style="vertical-align: middle;">
#if $cur_result['imdb_id']:
#if sickbeard.USE_IMDB_INFO and $cur_result['imdb_id']:
<a href="<%= anon_url('http://www.imdb.com/title/', cur_result['imdb_id']) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false" title="http://www.imdb.com/title/${cur_result['imdb_id']}"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" />
#end if
<a href="<%= anon_url(sickbeard.indexerApi(cur_indexer).config['show_url'], cur_result['showid']) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false" title="$sickbeard.indexerApi($cur_indexer).config['show_url']${cur_result['showid']}"><img alt="$sickbeard.indexerApi($cur_indexer).name" height="16" width="16" src="$sbRoot/images/$sickbeard.indexerApi($cur_indexer).config['icon']" /></a>
@ -558,7 +558,7 @@
</a></span>
<span class="tvshowTitleIcons">
#if $cur_result['imdb_id']:
#if sickbeard.USE_IMDB_INFO and $cur_result['imdb_id']:
<a href="<%= anon_url('http://www.imdb.com/title/', cur_result['imdb_id']) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false" title="http://www.imdb.com/title/${cur_result['imdb_id']}"><img alt="[imdb]" height="16" width="16" src="$sbRoot/images/imdb.png" />
#end if
<a href="<%= anon_url(sickbeard.indexerApi(cur_indexer).config['show_url'], cur_result['showid']) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false" title="$sickbeard.indexerApi($cur_indexer).config['show_url']${cur_result['showid']}"><img alt="$sickbeard.indexerApi($cur_indexer).name" height="16" width="16" src="$sbRoot/images/$sickbeard.indexerApi($cur_indexer).config['icon']" /></a>

View file

@ -7,13 +7,13 @@
#from sickbeard import sbdatetime
#from sickbeard.providers import generic
#from sickbeard.common import *
#set global $title="History"
#set global $header="History"
#set global $sbPath=".."
#set global $topmenu="history"#
#set global $title = 'History'
#set global $header = 'History'
#set global $sbPath = '..'
#set global $topmenu = 'history'
#set $layout = $sickbeard.HISTORY_LAYOUT
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
<style type="text/css">
.sort_data {display:none}
@ -23,93 +23,93 @@
<!--
\$.tablesorter.addParser({
id: 'cDate',
is: function(s) {
return false;
},
format: function(s) {
return s;
},
type: 'numeric'
id: 'cDate',
is: function(s) {
return false;
},
format: function(s) {
return s;
},
type: 'numeric'
});
\$(document).ready(function()
{
\$("#historyTable:has(tbody tr)").tablesorter({
widgets: ['zebra', 'filter'],
sortList: [[0,1]],
textExtraction: {
#if ( $layout == 'detailed'):
0: function(node) { return \$(node).find("span").text().toLowerCase(); },
4: function(node) { return \$(node).find("span").text().toLowerCase(); }
#else
0: function(node) { return \$(node).find("span").text().toLowerCase(); },
1: function(node) { return \$(node).find("span").text().toLowerCase(); },
2: function(node) { return \$(node).attr("provider").toLowerCase(); },
5: function(node) { return \$(node).attr("quality").toLowerCase(); }
#end if
},
headers: {
#if ( $layout == 'detailed'):
0: { sorter: 'cDate' },
4: { sorter: 'quality' }
#else
0: { sorter: 'cDate' },
4: { sorter: false },
5: { sorter: 'quality' }
#end if
}
});
\$('#limit').change(function(){
url = '$sbRoot/history/?limit='+\$(this).val()
window.location.href = url
});
\$(document).ready(function()
{
\$('#historyTable:has(tbody tr)').tablesorter({
widgets: ['zebra', 'filter'],
sortList: [[0, 1]],
textExtraction: {
0: function(node) { return \$(node).find('span').text().toLowerCase(); },
#if ('detailed' == $layout)
4: function(node) { return \$(node).find('span').text().toLowerCase(); }
#else
1: function(node) { return \$(node).find('span').text().toLowerCase(); },
2: function(node) { return \$(node).attr('provider').toLowerCase(); },
5: function(node) { return \$(node).attr('quality').toLowerCase(); }
#end if
},
headers: {
0: { sorter: 'cDate' },
#if ('detailed' == $layout)
4: { sorter: 'quality' }
#else
4: { sorter: false },
5: { sorter: 'quality' }
#end if
}
#set $fuzzydate = 'airdate'
#if $sickbeard.FUZZY_DATING:
fuzzyMoment({
containerClass : '.${fuzzydate}',
dateHasTime : true,
dateFormat : '${sickbeard.DATE_PRESET}',
timeFormat : '${sickbeard.TIME_PRESET_W_SECONDS}',
trimZero : #if $sickbeard.TRIM_ZERO then "true" else "false"#,
dtGlue : ', ',
});
#end if
});
\$('#limit').change(function(){
url = '$sbRoot/history/?limit=' + \$(this).val()
window.location.href = url
});
#set $fuzzydate = 'airdate'
#if $sickbeard.FUZZY_DATING
fuzzyMoment({
containerClass : '.${fuzzydate}',
dateHasTime : true,
dateFormat : '${sickbeard.DATE_PRESET}',
timeFormat : '${sickbeard.TIME_PRESET_W_SECONDS}',
trimZero : #echo ('false', 'true')[$sickbeard.TRIM_ZERO]#,
dtGlue : ', '
});
#end if
});
//-->
</script>
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#end if
<div class="h2footer pull-right"><b>Limit:</b>
<select name="limit" id="limit" class="form-control form-control-inline input-sm">
<option value="100" #if $limit == "100" then "selected=\"selected\"" else ""#>100</option>
<option value="250" #if $limit == "250" then "selected=\"selected\"" else ""#>250</option>
<option value="500" #if $limit == "500" then "selected=\"selected\"" else ""#>500</option>
<option value="0" #if $limit == "0" then "selected=\"selected\"" else ""#>All</option>
</select>
#set $html_selected = ' selected="selected"'
<span> Layout:
<select name="HistoryLayout" class="form-control form-control-inline input-sm" onchange="location = this.options[this.selectedIndex].value;">
<option value="$sbRoot/setHistoryLayout/?layout=compact" #if $sickbeard.HISTORY_LAYOUT == "compact" then "selected=\"selected\"" else ""#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed" #if $sickbeard.HISTORY_LAYOUT == "detailed" then "selected=\"selected\"" else ""#>Detailed</option>
<div class="h2footer pull-right">Limit:
<select name="limit" id="limit" class="form-control form-control-inline input-sm">
<option value="100"#echo ('', $html_selected)['100' == $limit]#>100</option>
<option value="250"#echo ('', $html_selected)['250' == $limit]#>250</option>
<option value="500"#echo ('', $html_selected)['500' == $limit]#>500</option>
<option value="0"#echo ('', $html_selected)['0' == $limit]#>All</option>
</select>
</span>
</div>
<br>
#if $layout == "detailed"
<table id="historyTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<span style="margin-left:5px">Layout:
<select name="HistoryLayout" class="form-control form-control-inline input-sm" onchange="location = this.options[this.selectedIndex].value">
<option value="$sbRoot/setHistoryLayout/?layout=compact"#echo ('', $html_selected)['compact' == $sickbeard.HISTORY_LAYOUT]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed"#echo ('', $html_selected)['detailed' == $sickbeard.HISTORY_LAYOUT]#>Detailed</option>
</select>
</span>
</div>
<br>
<table id="historyTable" class="sickbeardTable tablesorter $layout" cellspacing="1" border="0" cellpadding="0">
#if 'detailed' == $layout
<thead>
<tr>
<th class="nowrap">Time</th>
<th>Episode</th>
<th width="35%">Episode</th>
<th>Action</th>
<th>Provider</th>
<th>Quality</th>
@ -121,127 +121,142 @@
<th class="nowrap" colspan="5">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $historyResults:
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem["action"]))
#for $hItem in $historyResults
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem['action']))
<tr>
#set $curdatetime = $datetime.datetime.strptime(str($hItem["date"]), $history.dateFormat)
<td align="center"><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort_data">$time.mktime($curdatetime.timetuple())</span></td>
<td class="tvShow" width="35%"><a href="$sbRoot/home/displayShow?show=$hItem["showid"]#season-$hItem["season"]">$hItem["show_name"] - <%="S%02i" % int(hItem["season"])+"E%02i" % int(hItem["episode"]) %>#if "proper" in $hItem["resource"].lower or "repack" in $hItem["resource"].lower then ' <span class="quality Proper">Proper</span>' else ""#</a></td>
<td align="center" #if $curStatus == SUBTITLED then 'class="subtitles_column"' else ''#>
#if $curStatus == SUBTITLED:
<img width="16" height="11" style="vertical-align:middle;" src="$sbRoot/images/flags/<%= hItem["resource"][len(hItem["resource"])-6:len(hItem["resource"])-4]+'.png'%>">
#end if
<span style="cursor: help; vertical-align:middle;" title="$os.path.basename($hItem["resource"])">$statusStrings[$curStatus]</span>
#set $curdatetime = $datetime.datetime.strptime(str($hItem['date']), $history.dateFormat)
<td><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort_data">$time.mktime($curdatetime.timetuple())</span></td>
<td class="tvShow"><a href="$sbRoot/home/displayShow?show=$hItem['showid']#season-$hItem['season']">$hItem['show_name'] - <%= 'S%02i' % int(hItem['season']) + 'E%02i' % int(hItem['episode']) %>#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' <span class="quality Proper">Proper</span>' else ''#</a></td>
<td#echo ('', ' class="subtitles_column"')[$curStatus == SUBTITLED]#>
#if SUBTITLED == $curStatus
<img width="16" height="11" src="$sbRoot/images/flags/<%= hItem["resource"][len(hItem["resource"])-6:len(hItem["resource"])-4] + '.png' %>">
#end if
<span class="help" title="$os.path.basename($hItem["resource"])">$statusStrings[$curStatus]</span>
</td>
<td align="center">
#if $curStatus == DOWNLOADED:
#if $hItem["provider"] != "-1":
<span style="vertical-align:middle;"><i>$hItem["provider"]</i></span>
#end if
#else
#if $hItem["provider"] > 0
#if $curStatus in [SNATCHED, FAILED]:
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem["provider"]))
#if $provider != None:
<img src="$sbRoot/images/providers/<%=provider.imageName()%>" width="16" height="16" style="vertical-align:middle;" /> <span style="vertical-align:middle;">$provider.name</span>
#else:
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" style="vertical-align:middle;" title="missing provider"/> <span style="vertical-align:middle;">Missing Provider</span>
#end if
#else:
<img src="$sbRoot/images/subtitles/<%=hItem["provider"]+'.png' %>" width="16" height="16" style="vertical-align:middle;" /> <span style="vertical-align:middle;"><%=hItem["provider"].capitalize()%></span>
#end if
#end if
#end if
<td class="provider">
#if DOWNLOADED == $curStatus
#if '-1' != $hItem['provider']
<span><i>$hItem['provider']</i></span>
#end if
#else
#if 0 < $hItem['provider']
#if $curStatus in [SNATCHED, FAILED]
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($hItem['provider']))
#if None is not $provider
<img src="$sbRoot/images/providers/<%= provider.imageName() %>" width="16" height="16" /><span>$provider.name</span>
#else
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" title="missing provider" /><span>Missing Provider</span>
#end if
#else
<img src="$sbRoot/images/subtitles/<%= hItem['provider']+'.png' %>" width="16" height="16" /><span><%= hItem['provider'].capitalize() %></span>
#end if
#end if
#end if
</td>
<span style="display: none;">$curQuality</span>
<td align="center"><span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td>
<td><span style="display:none">$curQuality</span><span class="quality $Quality.qualityStrings[$curQuality].replace('720p', 'HD720p').replace('1080p', 'HD1080p').replace('RawHD TV', 'RawHD').replace('HD TV', 'HD720p')">$Quality.qualityStrings[$curQuality]</span></td>
</tr>
#end for
</tbody>
</table>
#end for
#else:
<table id="historyTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
#else
<thead>
<tr>
<th class="nowrap">Time</th>
<th>Episode</th>
<th width="#echo '3%s%%' % ('5', '0')[sickbeard.USE_SUBTITLES]#">Episode</th>
<th>Snatched</th>
<th>Downloaded</th>
#if sickbeard.USE_SUBTITLES
#if sickbeard.USE_SUBTITLES
<th>Subtitled</th>
#end if
<th>Quality</th>
#end if
<th width="14%">Quality</th>
</tr>
</thead>
<tfoot>
<tr>
<th class="nowrap" colspan="6">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $compactResults:
#for $hItem in $compactResults
#set $curdatetime = $datetime.datetime.strptime(str($hItem['actions'][0]['time']), $history.dateFormat)
#set $prov_list = []
#set $down_list = []
#set $order = 1
#set $ordinal_indicators = {'1':'st', '2':'nd', '3':'rd'}
#for $action in reversed($hItem['actions'])
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action']))
#set $basename = $os.path.basename($action['resource'])
#if $curStatus in [SNATCHED, FAILED]
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($action['provider']))
#if None is not $provider
#set $prov_list += ['<img class="help" src="%s/images/providers/%s" width="16" height="16" alt="%s" title="%s%s.. %s: %s" />'\
% ($sbRoot, $provider.imageName(), $provider.name, $order,
'th' if str($order)[-1] not in $ordinal_indicators else $ordinal_indicators[str($order)[-1]],
$provider.name, $basename)]
#set $order += 1
#else
#set $prov_list += ['<img src="%s/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider" />'\
% $sbRoot]
#end if
#end if
#if DOWNLOADED == $curStatus
#set $match = $re.search('\-(\w+)\.\w{3}\Z', $basename)
#set $non_scene_note = ''
#if not $match
## This fallback is for when idiots add a space and word to a release group. The space is converted
## to '\' which makes the regex parsing the scene group name fail, therefore we arrive here.
## A better solution would be to find where such data is parsed and saved to the db and perhaps
## fix at that point. But, in the meantime...
#set $non_scene_resource = re.sub(r'(\-\w+)([\\]\w+)?(\.\w{3})\Z', r'\1\3', $action['resource'])
#if $non_scene_resource
#set $non_scene_note = ' (Non scene name: %s)' % $action['resource'].partition('-')[-1]
#set $basename = $os.path.basename($non_scene_resource)
#set $match = $re.search('\-(\w+)\.\w{3}\Z', $basename)
#end if
#end if
#if $match
#if $match.group(1).upper() in ('X264', '720P')
#set $match = $re.search('(\w+)\-.*\-' + $match.group(1) + '\.\w{3}\Z', $os.path.basename($hItem['resource']), re.I)
#end if
#if $match
#set $down_list += ['<span class="help" title="%s%s"><i>%s</i></span>'\
% ($basename, $non_scene_note, $match.group(1).upper())]
#end if
#end if
#end if
#end for
<tr>
#set $curdatetime = $datetime.datetime.strptime(str($hItem["actions"][0]["time"]), $history.dateFormat)
<td align="center"><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort_data">$time.mktime($curdatetime.timetuple())</span></td>
<td class="tvShow" width="25%">
<span><a href="$sbRoot/home/displayShow?show=$hItem["show_id"]#season-$hItem["season"]">$hItem["show_name"] - <%="S%02i" % int(hItem["season"])+"E%02i" % int(hItem["episode"]) %>#if "proper" in $hItem["resource"].lower or "repack" in $hItem["resource"].lower then ' <span class="quality Proper">Proper</span>' else ""#</a></span>
<td><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort_data">$time.mktime($curdatetime.timetuple())</span></td>
<td class="tvShow">
<span><a href="$sbRoot/home/displayShow?show=$hItem['show_id']#season-$hItem['season']">$hItem['show_name'] - <%= 'S%02i' % int(hItem['season']) + 'E%02i' % int(hItem['episode']) %>#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' <span class="quality Proper">Proper</span>' else ''#</a></span>
</td>
<td align="center" provider="<%=str(sorted(hItem["actions"])[0]["provider"])%>">
#for $action in sorted($hItem["actions"]):
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action["action"]))
#if $curStatus in [SNATCHED, FAILED]:
#set $provider = $providers.getProviderClass($generic.GenericProvider.makeID($action["provider"]))
#if $provider != None:
<img src="$sbRoot/images/providers/<%=provider.imageName()%>" width="16" height="16" style="vertical-align:middle;" alt="$provider.name" style="cursor: help;" title="$provider.name: $os.path.basename($action["resource"])"/>
#else:
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" style="vertical-align:middle;" alt="missing provider" title="missing provider"/>
#end if
#end if
#end for
</td>
<td align="center">
#for $action in sorted($hItem["actions"]):
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action["action"]))
#if $curStatus == DOWNLOADED:
#set $match = $re.search("\-(\w+)\.\w{3}\Z", $os.path.basename($action["resource"]))
#if $match
#if $match.group(1).upper() in ("X264", "720P"):
#set $match = $re.search("(\w+)\-.*\-"+$match.group(1)+"\.\w{3}\Z", $os.path.basename($hItem["resource"]), re.IGNORECASE)
#if $match
<span style="cursor: help;" title="$os.path.basename($action["resource"])"><i>$match.group(1).upper()</i></span>
#end if
#else:
<span style="cursor: help;" title="$os.path.basename($action["resource"])"><i>$match.group(1).upper()</i></span>
#end if
#end if
#end if
#end for
<td class="provider" provider="<%= str(sorted(hItem['actions'])[0]['provider']) %>">
#echo ''.join($prov_list)#
</td>
#if sickbeard.USE_SUBTITLES:
<td align="center">
#for $action in sorted($hItem["actions"]):
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action["action"]))
#if $curStatus == SUBTITLED:
<img src="$sbRoot/images/subtitles/<%=action["provider"]+'.png' %>" width="16" height="16" style="vertical-align:middle;" alt="$action["provider"]" title="<%=action["provider"].capitalize()%>: $os.path.basename($action["resource"])"/>
<span style="vertical-align:middle;"> / </span>
<img width="16" height="11" style="vertical-align:middle;" src="$sbRoot/images/flags/<%= action["resource"][len(action["resource"])-6:len(action["resource"])-4]+'.png'%>" style="vertical-align: middle !important;">
&nbsp;
#end if
#end for
<td>
#echo ' '.join($down_list)#
</td>
#end if
<td align="center" width="14%" quality="$curQuality"><span class="quality $Quality.qualityStrings[$curQuality].replace("720p","HD720p").replace("1080p","HD1080p").replace("RawHD TV", "RawHD").replace("HD TV", "HD720p")">$Quality.qualityStrings[$curQuality]</span></td>
#if sickbeard.USE_SUBTITLES
<td>
#for $action in reversed($hItem['actions'])
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action']))
#if SUBTITLED == $curStatus
<img src="$sbRoot/images/subtitles/<%= action['provider'] + '.png' %>" width="16" height="16" alt="$action['provider']" title="<%= action['provider'].capitalize() %>:$os.path.basename($action['resource'])" />
<span> / </span>
<img width="16" height="11" src="$sbRoot/images/flags/<%= action['resource'][len(action['resource'])-6:len(action['resource'])-4] + '.png' %>" style="vertical-align:middle !important">
&nbsp;
#end if
#end for
</td>
#end if
<td quality="$curQuality"><span class="quality $Quality.qualityStrings[$curQuality].replace('720p', 'HD720p').replace('1080p', 'HD1080p').replace('RawHD TV', 'RawHD').replace('HD TV', 'HD720p')">$Quality.qualityStrings[$curQuality]</span></td>
</tr>
#end for
</tbody>
</table>
#end for
#end if
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
</tbody>
</table>
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -1,7 +1,7 @@
#import sickbeard
#import datetime
#from sickbeard.common import *
#from sickbeard import db, sbdatetime, network_timezones
#from sickbeard import sbdatetime, network_timezones
#set global $title="Home"
#set global $header="Show List"
@ -12,38 +12,6 @@
#import os.path
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#set $myDB = $db.DBConnection()
#set $today = str($datetime.date.today().toordinal())
#set $layout = $sickbeard.HOME_LAYOUT
#set status_quality = '(' + ','.join([str(x) for x in $Quality.SNATCHED + $Quality.SNATCHED_PROPER]) + ')'
#set status_download = '(' + ','.join([str(x) for x in $Quality.DOWNLOADED + [$ARCHIVED]]) + ')'
#set $sql_statement = 'SELECT showid, '
#set $sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 AND status IN ' + $status_quality + ') AS ep_snatched, '
#set $sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 AND status IN ' + $status_download + ') AS ep_downloaded, '
#set $sql_statement += '(SELECT COUNT(*) FROM tv_episodes WHERE showid=tv_eps.showid AND season > 0 AND episode > 0 AND airdate > 1 '
#set $sql_statement += ' AND ((airdate <= ' + $today + ' AND (status = ' + str($SKIPPED) + ' OR status = ' + str($WANTED) + ' OR status = ' + str($FAILED) + ')) '
#set $sql_statement += ' OR (status IN ' + status_quality + ') OR (status IN ' + status_download + '))) AS ep_total, '
#set $sql_statement += ' (SELECT airdate FROM tv_episodes WHERE showid=tv_eps.showid AND airdate >= ' + $today + ' AND (status = ' + str($UNAIRED) + ' OR status = ' + str($WANTED) + ') ORDER BY airdate ASC LIMIT 1) AS ep_airs_next '
#set $sql_statement += ' FROM tv_episodes tv_eps GROUP BY showid'
#set $sql_result = $myDB.select($sql_statement)
#set $show_stat = {}
#set $max_download_count = 1000
#for $cur_result in $sql_result:
#set $show_stat[$cur_result['showid']] = $cur_result
#if $cur_result['ep_total'] > $max_download_count:
#set $max_download_count = $cur_result['ep_total']
#end if
#end for
#set $max_download_count = $max_download_count * 100
<script type="text/javascript" charset="utf-8">
<!--

View file

@ -75,6 +75,8 @@
<div id="tableDiv"></div>
<br />
<p>If you tried to add a show, arrived here and can't see the folder, then that show may already be in your show list.</p>
<input class="btn btn-primary" type="button" value="Submit" id="submitShowDirs" />
</form>

View file

@ -1,10 +1,10 @@
#import sickbeard
#from sickbeard.common import *
#from sickbeard import subtitles
<div class="stepDiv linefix">
<div class="field-pair">
<label for="statusSelect">
<span class="component-title input">Initial episode status</span>
<span class="component-title">Initial episode status</span>
<span class="component-desc">
<select name="defaultStatus" id="statusSelect" class="form-control form-control-inline input-sm">
#for $curStatus in [$SKIPPED, $WANTED, $ARCHIVED, $IGNORED]:
@ -26,6 +26,28 @@
#set global $bestQualities = $qualities[1]
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_qualityChooser.tmpl')
</div>
#try:
#if True == $enable_default_wanted:
<div class="field-pair alt">
<span class="component-title">From season 1 forward, set</span>
<span class="component-desc">
<label for="wanted_begin" style="padding-bottom:10px">
<input type="number" name="wanted_begin" id="wanted_begin" value="$sickbeard.WANTED_BEGIN_DEFAULT" class="form-control input-sm input75">
<span>episodes as wanted (10 ... 0, and where -1 is whole first season)</span>
</label>
</span>
<span class="component-title">From latest going back, set</span>
<span class="component-desc">
<label for="wanted_latest">
<input type="number" name="wanted_latest" id="wanted_latest" value="$sickbeard.WANTED_LATEST_DEFAULT" class="form-control input-sm input75">
<span>episodes as wanted (10 ... 0, and where -1 is whole latest season)</span>
</label>
</span>
</div>
#end if
#except (NameError, NotFound):
#pass
#end try
<div class="field-pair alt">
<label for="flatten_folders">
@ -80,10 +102,11 @@
</span>
</label>
</div>
</div>
#if $enable_anime_options
#import sickbeard.blackandwhitelist
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_blackwhitelist.tmpl')
#else
<input type="hidden" name="anime" id="anime" value="0" />
#end if
#end if

View file

@ -1,5 +1,6 @@
#import sickbeard
#import datetime
#import re
#from sickbeard import db, sbdatetime
#from sickbeard.common import *
@ -22,12 +23,12 @@
% ($today, str($SKIPPED), str($WANTED))\
+ ' OR (status IN %s) OR (status IN %s))) AS ep_total FROM tv_episodes tv_eps LIMIT 1'\
% ($status_quality, $status_download)
#slurp
#set $sql_result = $my_db.select($sql_statement)
#slurp
#set $shows_total = len($sickbeard.showList)
#set $shows_active = len([show for show in $sickbeard.showList if 0 == show.paused and 'Ended' != show.status])
#slurp
#if $sql_result:
#set $ep_snatched = $sql_result[0]['ep_snatched']
#set $ep_downloaded = $sql_result[0]['ep_downloaded']
@ -37,8 +38,8 @@
#set $ep_downloaded = 0
#set $ep_total = 0
#end if
#set $ep_percentage = '' if $ep_total == 0 else '(<span class="footerhighlight">{:.1%}</span>)'.format(float($ep_downloaded)/float($ep_total))
#set $ep_percentage = '' if $ep_total == 0 else '(<span class="footerhighlight">%s%%</span>)' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str((float($ep_downloaded)/float($ep_total))*100))
#slurp
#try
#set $localRoot = $sbRoot
#except NotFound
@ -49,24 +50,20 @@
#except NotFound
#set $localheader = ''
#end try
#slurp
<span class="footerhighlight">$shows_total</span> shows (<span class="footerhighlight">$shows_active</span> active)
| <span class="footerhighlight">$ep_downloaded</span><%=
(
'',
' (<span class="footerhighlight">+%s</span> snatched)'\
% (
str(ep_snatched),
'<a href="%s/manage/episodeStatuses?whichStatus=2" title="View overview of snatched episodes">%s</a>'\
% (localRoot, str(ep_snatched))
)['Episode Overview' != localheader]
% '<a href="%s/manage/episodeStatuses?whichStatus=2" title="View overview of snatched episodes">%s</a>'
% (localRoot, str(ep_snatched))
)[0 < ep_snatched]
%>&nbsp;/&nbsp;<span class="footerhighlight">$ep_total</span> episodes downloaded $ep_percentage
| recent search: <span class="footerhighlight"><%= str(sickbeard.recentSearchScheduler.timeLeft()).split('.')[0] %></span>
| backlog search: <span class="footerhighlight"><%= str(sickbeard.backlogSearchScheduler.timeLeft()).split('.')[0] %></span>
#slurp
</div>
</footer>
</body>

View file

@ -60,8 +60,6 @@
<script type="text/javascript" src="$sbRoot/js/lib/imagesloaded.pkgd.min.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/lib/jquery.confirm.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/script.js?$sbPID"></script>
#if $sickbeard.FUZZY_DATING:
<script type="text/javascript" src="$sbRoot/js/moment/moment.min.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/fuzzyMoment.js?$sbPID"></script>
@ -81,35 +79,37 @@
<script type="text/javascript">
<!--
function initActions() {
\$("#SubMenu a[href*='/home/restart/']").addClass('btn restart').html('<span class="submenu-icon-restart pull-left"></span> Restart');
\$("#SubMenu a[href*='/home/shutdown/']").addClass('btn shutdown').html('<span class="submenu-icon-shutdown pull-left"></span> Shutdown');
\$("#SubMenu a[href*='/home/logout/']").addClass('btn').html('<span class="ui-icon ui-icon-power pull-left"></span> Logout');
\$("#SubMenu a:contains('Edit')").addClass('btn').html('<span class="ui-icon ui-icon-pencil pull-left"></span> Edit');
\$("#SubMenu a:contains('Remove')").addClass('btn remove').html('<span class="ui-icon ui-icon-trash pull-left"></span> Remove');
\$("#SubMenu a:contains('Clear History')").addClass('btn clearhistory').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear History');
\$("#SubMenu a:contains('Trim History')").addClass('btn trimhistory').html('<span class="ui-icon ui-icon-trash pull-left"></span> Trim History');
\$("#SubMenu a[href$='/errorlogs/clearerrors/']").addClass('btn').html('<span class="ui-icon ui-icon-trash pull-left"></span> Clear Errors');
\$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Re-scan');
\$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<span class="ui-icon ui-icon-refresh pull-left"></span> Backlog Overview');
\$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html('<span class="submenu-icon-plex pull-left"></span> Update PLEX');
\$("#SubMenu a:contains('Force')").addClass('btn').html('<span class="ui-icon ui-icon-transfer-e-w pull-left"></span> Force Full Update');
\$("#SubMenu a:contains('Rename')").addClass('btn').html('<span class="ui-icon ui-icon-tag pull-left"></span> Preview Rename');
\$("#SubMenu a[href$='/config/subtitles/']").addClass('btn').html('<span class="ui-icon ui-icon-comment pull-left"></span> Search Subtitles');
\$("#SubMenu a[href*='/home/subtitleShow']").addClass('btn').html('<span class="ui-icon ui-icon-comment pull-left"></span> Download Subtitles');
\$("#SubMenu a:contains('Anime')").addClass('btn').html('<span class="submenu-icon-anime pull-left"></span> Anime');
\$("#SubMenu a:contains('Settings')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Settings');
\$("#SubMenu a:contains('Provider')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Providers');
\$("#SubMenu a:contains('General')").addClass('btn').html('<span class="ui-icon ui-icon-gear pull-left"></span> General');
\$("#SubMenu a:contains('Episode Status')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Episode Status Management');
\$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Missed Subtitles');
\$("#SubMenu a[href$='/home/addShows/']").addClass('btn').html('<span class="ui-icon ui-icon-video pull-left"></span> Add Show');
\$("#SubMenu a:contains('Processing')").addClass('btn').html('<span class="ui-icon ui-icon-folder-open pull-left"></span> Post-Processing');
\$("#SubMenu a:contains('Manage Searches')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Manage Searches');
\$("#SubMenu a:contains('Manage Torrents')").addClass('btn').html('<span class="submenu-icon-bittorrent pull-left"></span> Manage Torrents');
\$("#SubMenu a[href$='/manage/failedDownloads/']").addClass('btn').html('<span class="submenu-icon-failed-download pull-left"></span> Failed Downloads');
\$("#SubMenu a:contains('Notification')").addClass('btn').html('<span class="ui-icon ui-icon-note pull-left"></span> Notifications');
\$("#SubMenu a:contains('Update show in XBMC')").addClass('btn').html('<span class="submenu-icon-xbmc pull-left"></span> Update show in XBMC');
\$("#SubMenu a[href$='/home/updateXBMC/']").addClass('btn').html('<span class="submenu-icon-xbmc pull-left"></span> Update XBMC');
\$("#SubMenu a[href*='/home/restart/']").addClass('btn restart').html('<i class="sgicon-restart"></i> Restart');
\$("#SubMenu a[href*='/home/shutdown/']").addClass('btn shutdown').html('<i class="sgicon-shutdown"></i> Shutdown');
\$("#SubMenu a[href*='/home/logout/']").addClass('btn').html('<i class="sgicon-logout"></i> Logout');
\$("#SubMenu a:contains('Edit')").addClass('btn').html('<i class="sgicon-edit"></i> Edit');
\$("#SubMenu a:contains('Remove')").addClass('btn remove').html('<i class="sgicon-delete"></i> Remove');
\$("#SubMenu a:contains('Clear History')").addClass('btn clearhistory').html('<i class="sgicon-delete"></i> Clear History');
\$("#SubMenu a:contains('Trim History')").addClass('btn trimhistory').html('<i class="sgicon-trim"></i> Trim History');
\$("#SubMenu a[href$='/errorlogs/clearerrors/']").addClass('btn').html('<i class="sgicon-delete"></i> Clear Errors');
\$("#SubMenu a:contains('Re-scan')").addClass('btn').html('<i class="sgicon-refresh"></i> Re-scan');
\$("#SubMenu a:contains('Backlog Overview')").addClass('btn').html('<i class="sgicon-backlog"></i> Backlog Overview');
\$("#SubMenu a[href$='/home/updatePLEX/']").addClass('btn').html('<i class="sgicon-plex"></i>Update PLEX');
\$("#SubMenu a:contains('Force')").addClass('btn').html('<i class="sgicon-fullupdate"></i> Force Full Update');
\$("#SubMenu a:contains('Rename')").addClass('btn').html('<i class="sgicon-rename"></i> Preview Rename');
\$("#SubMenu a[href$='/config/subtitles/']").addClass('btn').html('<i class="sgicon-subtitles"></i> Search Subtitles');
\$("#SubMenu a[href*='/home/subtitleShow']").addClass('btn').html('<i class="sgicon-subtitles"></i> Download Subtitles');
\$("#SubMenu a:contains('Anime')").addClass('btn').html('<i class="sgicon-anime"></i> Anime');
\$("#SubMenu a:contains('Settings')").addClass('btn').html('<i class="sgicon-search"></i> Search Settings');
\$("#SubMenu a:contains('Provider')").addClass('btn').html('<i class="sgicon-search"></i> Search Providers');
\$("#SubMenu a:contains('General')").addClass('btn').html('<i class="sgicon-config"></i> General');
\$("#SubMenu a:contains('Episode Status')").addClass('btn').html('<i class="sgicon-episodestatus"></i> Episode Status Management');
\$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html('<i class="sgicon-subtitles"></i> Missed Subtitles');
\$("#SubMenu a[href$='/home/addShows/']").addClass('btn').html('<i class="sgicon-addshow"></i> Add Show');
\$("#SubMenu a:contains('Processing')").addClass('btn').html('<i class="sgicon-postprocess"></i> Post-Processing');
\$("#SubMenu a:contains('Manage Searches')").addClass('btn').html('<i class="sgicon-search"></i> Manage Searches');
\$("#SubMenu a:contains('Manage Torrents')").addClass('btn').html('<i class="sgicon-bittorrent"></i> Manage Torrents');
\$("#SubMenu a[href$='/manage/failedDownloads/']").addClass('btn').html('<i class="sgicon-failed"></i> Failed Downloads');
\$("#SubMenu a:contains('Notification')").addClass('btn').html('<i class="sgicon-notification"></i> Notifications');
\$("#SubMenu a:contains('Update show in XBMC')").addClass('btn').html('<i class="sgicon-xbmc"></i> Update show in XBMC');
\$("#SubMenu a[href$='/home/updateXBMC/']").addClass('btn').html('<i class="sgicon-xbmc"></i> Update XBMC');
\$("#SubMenu a:contains('Update show in Kodi')").addClass('btn').html('<i class="sgicon-kodi"></i> Update show in Kodi');
\$("#SubMenu a[href$='/home/updateKODI/']").addClass('btn').html('<i class="sgicon-kodi"></i> Update Kodi');
}
\$(document).ready(function() {
@ -126,7 +126,13 @@
<script type="text/javascript" src="$sbRoot/js/confirmations.js?$sbPID"></script>
</head>
#set $tab = 4
<body>
#set $body_attr = ''
#try
#set $body_attr += ' id="%s"' % $page_body_attr
#except
#pass
#end try
<body$body_attr>
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
@ -144,9 +150,9 @@
<li id="NAVhome" class="dropdown">
<a href="$sbRoot/home/" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#">Shows <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/home/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-home"></i>&nbsp;Show List</a></li>
<li><a href="$sbRoot/home/addShows/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-addshow"></i>&nbsp;Add Shows</a></li>
<li><a href="$sbRoot/home/postprocess/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-postprocess"></i>&nbsp;Manual Post-Processing</a></li>
<li><a href="$sbRoot/home/showlistView/" tabindex="$tab#set $tab += 1#"><i class="sgicon-home"></i>&nbsp;Show List</a></li>
<li><a href="$sbRoot/home/addShows/" tabindex="$tab#set $tab += 1#"><i class="sgicon-addshow"></i>&nbsp;Add Shows</a></li>
<li><a href="$sbRoot/home/postprocess/" tabindex="$tab#set $tab += 1#"><i class="sgicon-postprocess"></i>&nbsp;Manual Post-Processing</a></li>
</ul>
</li>
@ -161,26 +167,29 @@
<li id="NAVmanage" class="dropdown">
<a href="$sbRoot/manage/" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#">Manage <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/manage/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-manage"></i>&nbsp;Mass Update</a></li>
<li><a href="$sbRoot/manage/backlogOverview/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-backlog-view"></i>&nbsp;Backlog Overview</a></li>
<li><a href="$sbRoot/manage/manageSearches/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-manage-searches"></i>&nbsp;Manage Searches</a></li>
<li><a href="$sbRoot/manage/episodeStatuses/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-backlog"></i>&nbsp;Episode Status Management</a></li>
#if $sickbeard.USE_PLEX and $sickbeard.PLEX_SERVER_HOST != "":
<li><a href="$sbRoot/home/updatePLEX/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-plex"></i>&nbsp;Update PLEX</a></li>
<li><a href="$sbRoot/manage/" tabindex="$tab#set $tab += 1#"><i class="sgicon-massupdate"></i>&nbsp;Mass Update</a></li>
<li><a href="$sbRoot/manage/backlogOverview/" tabindex="$tab#set $tab += 1#"><i class="sgicon-backlog"></i>&nbsp;Backlog Overview</a></li>
<li><a href="$sbRoot/manage/manageSearches/" tabindex="$tab#set $tab += 1#"><i class="sgicon-search"></i>&nbsp;Manage Searches</a></li>
<li><a href="$sbRoot/manage/episodeStatuses/" tabindex="$tab#set $tab += 1#"><i class="sgicon-episodestatus"></i>&nbsp;Episode Status Management</a></li>
#if $sickbeard.USE_PLEX and $sickbeard.PLEX_SERVER_HOST != '':
<li><a href="$sbRoot/home/updatePLEX/" tabindex="$tab#set $tab += 1#"><i class="sgicon-plex"></i>&nbsp;Update PLEX</a></li>
#end if
#if $sickbeard.USE_XBMC and $sickbeard.XBMC_HOST != "":
<li><a href="$sbRoot/home/updateXBMC/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-xbmc"></i>&nbsp;Update XBMC</a></li>
#if $sickbeard.USE_XBMC and $sickbeard.XBMC_HOST != '':
<li><a href="$sbRoot/home/updateXBMC/" tabindex="$tab#set $tab += 1#"><i class="sgicon-xbmc"></i>&nbsp;Update XBMC</a></li>
#end if
#if $sickbeard.USE_KODI and $sickbeard.KODI_HOST != '':
<li><a href="$sbRoot/home/updateKODI/" tabindex="$tab#set $tab += 1#"><i class="sgicon-kodi"></i>&nbsp;Update Kodi</a></li>
#end if
#if $sickbeard.USE_TORRENTS and $sickbeard.TORRENT_METHOD != 'blackhole' \
and ($sickbeard.ENABLE_HTTPS and $sickbeard.TORRENT_HOST[:5] == 'https' \
or not $sickbeard.ENABLE_HTTPS and $sickbeard.TORRENT_HOST[:5] == 'http:'):
<li><a href="$sbRoot/manage/manageTorrents/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-bittorrent"></i>&nbsp;Manage Torrents</a></li>
<li><a href="$sbRoot/manage/manageTorrents/" tabindex="$tab#set $tab += 1#"><i class="sgicon-bittorrent"></i>&nbsp;Manage Torrents</a></li>
#end if
#if $sickbeard.USE_FAILED_DOWNLOADS:
<li><a href="$sbRoot/manage/failedDownloads/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-failed-download"></i>&nbsp;Failed Downloads</a></li>
<li><a href="$sbRoot/manage/failedDownloads/" tabindex="$tab#set $tab += 1#"><i class="sgicon-failed"></i>&nbsp;Failed Downloads</a></li>
#end if
#if $sickbeard.USE_SUBTITLES:
<li><a href="$sbRoot/manage/subtitleMissed/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-backlog"></i>&nbsp;Missed Subtitle Management</a></li>
<li><a href="$sbRoot/manage/subtitleMissed/" tabindex="$tab#set $tab += 1#"><i class="sgicon-subtitles"></i>&nbsp;Missed Subtitle Management</a></li>
#end if
</ul>
</li>
@ -188,34 +197,34 @@
<li id="NAVerrorlogs" class="dropdown">
<a href="$sbRoot/errorlogs/" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#">$logPageTitle <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/errorlogs/"><i class="menu-icon-viewlog-errors" tabindex="$tab#set $tab += 1#"></i>&nbsp;View Log (Errors)</a></li>
<li><a href="$sbRoot/errorlogs/viewlog/"><i class="menu-icon-viewlog" tabindex="$tab#set $tab += 1#"></i>&nbsp;View Log</a></li>
<li><a href="$sbRoot/errorlogs/" tabindex="$tab#set $tab += 1#"><i class="sgicon-errorlog"></i>&nbsp;View Log (Errors)</a></li>
<li><a href="$sbRoot/errorlogs/viewlog/" tabindex="$tab#set $tab += 1#"><i class="sgicon-log"></i>&nbsp;View Log</a></li>
</ul>
</li>
<li id="NAVconfig" class="dropdown">
<a href="$sbRoot/config/" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#"><img src="$sbRoot/images/menu/system18.png" class="navbaricon hidden-xs" /><b class="caret hidden-xs"></b><span class="visible-xs">Config <b class="caret"></b></span></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/config/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-help"></i>&nbsp;Help &amp; Info</a></li>
<li><a href="$sbRoot/config/general/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;General</a></li>
<li><a href="$sbRoot/config/search/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Search Settings</a></li>
<li><a href="$sbRoot/config/providers/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Search Providers</a></li>
<li><a href="$sbRoot/config/subtitles/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Subtitles Settings</a></li>
<li><a href="$sbRoot/config/postProcessing/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Post Processing</a></li>
<li><a href="$sbRoot/config/notifications/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Notifications</a></li>
<li><a href="$sbRoot/config/anime/" tabindex="$tab#set $tab += 1#"><i class="menu-icon-config"></i>&nbsp;Anime</a></li>
<li><a href="$sbRoot/config/" tabindex="$tab#set $tab += 1#"><i class="sgicon-info"></i>&nbsp;Help &amp; Info</a></li>
<li><a href="$sbRoot/config/general/" tabindex="$tab#set $tab += 1#"><i class="sgicon-config"></i>&nbsp;General</a></li>
<li><a href="$sbRoot/config/search/" tabindex="$tab#set $tab += 1#"><i class="sgicon-search"></i>&nbsp;Search Settings</a></li>
<li><a href="$sbRoot/config/providers/" tabindex="$tab#set $tab += 1#"><i class="sgicon-search"></i>&nbsp;Search Providers</a></li>
<li><a href="$sbRoot/config/subtitles/" tabindex="$tab#set $tab += 1#"><i class="sgicon-subtitles"></i>&nbsp;Subtitles Settings</a></li>
<li><a href="$sbRoot/config/postProcessing/" tabindex="$tab#set $tab += 1#"><i class="sgicon-postprocess"></i>&nbsp;Post Processing</a></li>
<li><a href="$sbRoot/config/notifications/" tabindex="$tab#set $tab += 1#"><i class="sgicon-notification"></i>&nbsp;Notifications</a></li>
<li><a href="$sbRoot/config/anime/" tabindex="$tab#set $tab += 1#"><i class="sgicon-anime"></i>&nbsp;Anime</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-delay="0" tabindex="$tab#set $tab += 1#"><img src="$sbRoot/images/menu/system18-2.png" class="navbaricon hidden-xs" /><b class="caret hidden-xs"></b><span class="visible-xs">System <b class="caret"></b></span></a>
<ul class="dropdown-menu">
<li><a href="$sbRoot/manage/manageSearches/forceVersionCheck" tabindex="$tab#set $tab += 1#"><i class="menu-icon-update"></i>&nbsp;Force Version Check</a></li>
<li><a href="$sbRoot/manage/manageSearches/forceVersionCheck" tabindex="$tab#set $tab += 1#"><i class="sgicon-updatecheck"></i>&nbsp;Force Version Check</a></li>
#if $sickbeard.WEB_USERNAME or $sickbeard.WEB_PASSWORD:
<li><a href="$sbRoot/logout" class="confirm logout" tabindex="$tab#set $tab += 1#"><i class="menu-icon-logout"></i>&nbsp;Logout</a></li>
<li><a href="$sbRoot/logout" class="confirm logout" tabindex="$tab#set $tab += 1#"><i class="sgicon-logout"></i>&nbsp;Logout</a></li>
#end if
<li><a href="$sbRoot/home/restart/?pid=$sbPID" class="confirm restart" tabindex="$tab#set $tab += 1#"><i class="menu-icon-restart"></i>&nbsp;Restart</a></li>
<li><a href="$sbRoot/home/shutdown/?pid=$sbPID" class="confirm shutdown" tabindex="$tab#set $tab += 1#"><i class="menu-icon-shutdown"></i>&nbsp;Shutdown</a></li>
<li><a href="$sbRoot/home/restart/?pid=$sbPID" class="confirm restart" tabindex="$tab#set $tab += 1#"><i class="sgicon-restart"></i>&nbsp;Restart</a></li>
<li><a href="$sbRoot/home/shutdown/?pid=$sbPID" class="confirm shutdown" tabindex="$tab#set $tab += 1#"><i class="sgicon-shutdown"></i>&nbsp;Shutdown</a></li>
</ul>
</li>
</ul>

View file

@ -1,80 +1,92 @@
#import sickbeard
#from sickbeard.common import *
#set global $title="Mass Update"
#set global $header="Mass Update"
#set global $title = 'Mass Update'
#set global $header = 'Mass Update'
#set global $sbPath="../.."
#set global $sbPath = '../..'
#set global $topmenu="manage"
#set global $topmenu = 'manage'
#import os.path
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
#set $has_any_sports = False
#set $has_any_anime = False
#set $has_any_flat_folders = False
#set $myShowList = $sickbeard.showList
$myShowList.sort(lambda x, y: cmp(x.name, y.name))
#for $curShow in $myShowList
#set $has_any_sports |= bool($curShow.sports)
#set $has_any_anime |= bool($curShow.anime)
#set $has_any_flat_folders |= $bool(curShow.flatten_folders)
#end for
<script type="text/javascript" charset="utf-8">
<!--
\$.tablesorter.addParser({
id: 'showNames',
is: function(s) {
return false;
},
format: function(s) {
#if not $sickbeard.SORT_ARTICLE:
id: 'showNames',
is: function(s) {
return false;
},
format: function(s) {
#if not $sickbeard.SORT_ARTICLE
return (s || '').replace(/^(?:(?:A(?!\s+to)n?)|The)\s(\w)/i, '$1');
#else:
#else
return (s || '');
#end if
},
type: 'text'
},
type: 'text'
});
\$.tablesorter.addParser({
id: 'quality',
is: function(s) {
return false;
},
format: function(s) {
return s.replace('hd1080p',5).replace('hd720p',4).replace('hd',3).replace('sd',2).replace('any',1).replace('best',0).replace('custom',7);
},
type: 'numeric'
id: 'quality',
is: function(s) {
return false;
},
format: function(s) {
return s.replace('hd1080p', 5).replace('hd720p', 4).replace('hd', 3).replace('sd', 2).replace('any', 1).replace('best', 0).replace('custom', 7);
},
type: 'numeric'
});
\$(document).ready(function()
{
\$("#massUpdateTable:has(tbody tr)").tablesorter({
sortList: [[1,0]],
textExtraction: {
2: function(node) { return \$(node).find("span").text().toLowerCase(); },
3: function(node) { return \$(node).find("img").attr("alt"); },
4: function(node) { return \$(node).find("img").attr("alt"); },
5: function(node) { return \$(node).find("img").attr("alt"); },
6: function(node) { return \$(node).find("img").attr("alt"); },
7: function(node) { return \$(node).find("img").attr("alt"); }
},
widgets: ['zebra'],
headers: {
0: { sorter: false},
1: { sorter: 'showNames'},
2: { sorter: 'quality'},
3: { sorter: 'sports'},
4: { sorter: 'scene'},
5: { sorter: 'anime'},
6: { sorter: 'flatfold'},
7: { sorter: 'paused'},
8: { sorter: 'status'},
9: { sorter: false},
10: { sorter: false},
11: { sorter: false},
12: { sorter: false},
13: { sorter: false},
14: { sorter: false},
15: { sorter: false}
}
});
#set $columns_total = 15 - ((1, 0)[$has_any_sports] + (1, 0)[$has_any_anime] + (1, 0)[$has_any_flat_folders] + (1, 0)[$sickbeard.USE_SUBTITLES])
#set $column_headers = [('false', False), ("'showNames'", False), ("'quality'", False),
((None, "'sports'")[$has_any_sports], True),
("'scene'", True), ((None, "'anime'")[$has_any_anime], True),
((None, "'flatfold'")[$has_any_flat_folders], True), ("'paused'", True),
("'status'", False), ('false', False), ('false', False), ('false', False),
((None, 'false')[$sickbeard.USE_SUBTITLES], False), ('false', False), ('false', False)]
#set $headers = []
#set $text_extract = []
#set $column = -1
#for $k, ($c, $img_extract) in enumerate($column_headers)
#if None is $c
#continue
#end if
#set $column += 1
$headers.append('\t\t\t%s: { sorter: %s }' % ($column, $c))
#if $img_extract
$text_extract.append('\t\t\t%s%s' % ($column, ": function(node) {return $(node).find('img').attr('alt')}"))
#end if
#end for
\$(document).ready(function()
{
\$('#massUpdateTable:has(tbody tr)').tablesorter({
widgets: ['zebra'],
sortList: [[1,0]],
headers: {
#echo ',\n'.join($headers)#
},
textExtraction: {
2: function(node) {return \$(node).find('span').text().toLowerCase()},
#echo ',\n'.join($text_extract)#
}
});
});
//-->
</script>
<script type="text/javascript" src="$sbRoot/js/massUpdate.js?$sbPID"></script>
#if $varExists('header')
#if $varExists('header')
<h1 class="header">$header</h1>
#else
#else
<h1 class="title">$title</h1>
#end if
<form name="massUpdateForm" method="post" action="massUpdate">
@ -82,113 +94,122 @@
<table id="massUpdateTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead>
<tr>
<th class="col-checkbox">Edit<br/><input type="checkbox" class="bulkCheck" id="editCheck" /></th>
<th class="nowrap" style="text-align: left;">Show Name</th>
<th class="col-legend">Quality</th>
<th class="col-legend">Sports</th>
<th class="col-legend">Scene</th>
<th class="col-legend">Anime</th>
<th class="col-legend">Flat Folders</th>
<th class="col-legend">Paused</th>
<th class="col-legend">Status</th>
<th width="1%">Update<br/><input type="checkbox" class="bulkCheck" id="updateCheck" /></th>
<th width="1%">Rescan<br/><input type="checkbox" class="bulkCheck" id="refreshCheck" /></th>
<th width="1%">Rename<br/><input type="checkbox" class="bulkCheck" id="renameCheck" /></th>
#if $sickbeard.USE_SUBTITLES:
<th width="1%">Subtitle<br/><input type="checkbox" class="bulkCheck" id="subtitleCheck" /></th>
#end if
<!-- <th>Force Metadata Regen <input type="checkbox" class="bulkCheck" id="metadataCheck" /></th>//-->
<th width="1%">Delete<br/><input type="checkbox" class="bulkCheck" id="deleteCheck" /></th>
<th width="1%">Remove<br/><input type="checkbox" class="bulkCheck" id="removeCheck" /></th>
<th class="col-checkbox">Edit<br /><input type="checkbox" class="bulkCheck" id="editCheck"></th>
<th class="nowrap narrow" style="text-align:left">Show Name</th>
<th class="col-legend narrow">Quality</th>
#if $has_any_sports
<th class="col-legend narrow">Sports</th>
#end if
<th class="col-legend narrow">Scene</th>
#if $has_any_anime
<th class="col-legend narrow">Anime</th>
#end if
#if $has_any_flat_folders
<th class="col-legend narrow">Flat<br /> Folders</th>
#end if
<th class="col-legend narrow">Paused</th>
<th class="col-legend narrow">Status</th>
<th width="1%">Update<br /><input type="checkbox" class="bulkCheck" id="updateCheck"></th>
<th width="1%">Rescan<br /><input type="checkbox" class="bulkCheck" id="refreshCheck"></th>
<th width="1%">Rename<br /><input type="checkbox" class="bulkCheck" id="renameCheck"></th>
#if $sickbeard.USE_SUBTITLES
<th width="1%">Search<br />Subtitle<br /><input type="checkbox" class="bulkCheck" id="subtitleCheck"></th>
#end if
## <!-- <th>Force Metadata Regen <input type="checkbox" class="bulkCheck" id="metadataCheck"></th>//-->
<th width="1%">Delete<br /><input type="checkbox" class="bulkCheck" id="deleteCheck"></th>
<th width="1%">Remove<br /><input type="checkbox" class="bulkCheck" id="removeCheck"></th>
</tr>
</thead>
<tfoot>
<tr>
<td rowspan="1" colspan="2" class="align-center alt"><input class="btn pull-left" type="button" value="Edit Selected" id="submitMassEdit" /></td>
<td rowspan="1" colspan="#if $sickbeard.USE_SUBTITLES then 13 else 12#" class="align-right alt"><input class="btn pull-right" type="button" value="Submit" id="submitMassUpdate" /></td>
<td rowspan="1" colspan="2" class="align-center alt"><input class="btn pull-left" type="button" value="Edit Selected" id="submitMassEdit"></td>
<td rowspan="1" colspan="#echo $columns_total-2#" class="align-right alt"><input class="btn pull-right" type="button" value="Submit" id="submitMassUpdate"></td>
</tr>
</tfoot>
<tbody>
#set $myShowList = $sickbeard.showList
$myShowList.sort(lambda x, y: cmp(x.name, y.name))
#for $curShow in $myShowList:
#set $curEp = $curShow.nextaired
#set $curUpdate_disabled = ""
#set $curRefresh_disabled = ""
#set $curRename_disabled = ""
#set $curSubtitle_disabled = ""
#set $curDelete_disabled = ""
#set $curRemove_disabled = ""
#if $sickbeard.showQueueScheduler.action.isBeingUpdated($curShow) or $sickbeard.showQueueScheduler.action.isInUpdateQueue($curShow):
#set $curUpdate_disabled = "disabled=\"disabled\" "
#end if
#set $curUpdate = "<input type=\"checkbox\" class=\"updateCheck\" id=\"update-"+str($curShow.indexerid)+"\" "+$curUpdate_disabled+"/>"
#if $sickbeard.showQueueScheduler.action.isBeingRefreshed($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow):
#set $curRefresh_disabled = "disabled=\"disabled\" "
#end if
#set $curRefresh = "<input type=\"checkbox\" class=\"refreshCheck\" id=\"refresh-"+str($curShow.indexerid)+"\" "+$curRefresh_disabled+"/>"
#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow):
#set $curRename = "disabled=\"disabled\" "
#end if
#set $curRename = "<input type=\"checkbox\" class=\"renameCheck\" id=\"rename-"+str($curShow.indexerid)+"\" "+$curRename_disabled+"/>"
#if not $curShow.subtitles or $sickbeard.showQueueScheduler.action.isBeingSubtitled($curShow) or $sickbeard.showQueueScheduler.action.isInSubtitleQueue($curShow):
#set $curSubtitle_disabled = "disabled=\"disabled\" "
#end if
#set $curSubtitle = "<input type=\"checkbox\" class=\"subtitleCheck\" id=\"subtitle-"+str($curShow.indexerid)+"\" "+$curSubtitle_disabled+"/>"
#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow):
#set $curDelete = "disabled=\"disabled\" "
#end if
#set $curDelete = "<input type=\"checkbox\" class=\"deleteCheck\" id=\"delete-"+str($curShow.indexerid)+"\" "+$curDelete_disabled+"/>"
#if $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow) or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow) or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow):
#set $curRemove = "disabled=\"disabled\" "
#end if
#set $curRemove = "<input type=\"checkbox\" class=\"removeCheck\" id=\"remove-"+str($curShow.indexerid)+"\" "+$curRemove_disabled+"/>"
#set $disabled = ' disabled="disabled"'
#set $disabled_inprogress_tip = ' title="%s action is currently in progress for this show"'
#set $disabled_subtitles_tip = ' title="Use edit to enable subtitle search for this show"'
#set $no = 'no16.png" title="No" alt="No'
#set $yes = 'yes16.png" title="Yes" alt="Yes'
#for $curShow in $myShowList
#set $option_state = '<input type="checkbox" class="%sCheck" id="%s-{0:s}"%s>'.format(str($curShow.indexerid))
#slurp
#set $curUpdate_disabled = $sickbeard.showQueueScheduler.action.isBeingUpdated($curShow)\
or $sickbeard.showQueueScheduler.action.isInUpdateQueue($curShow)
#set $reason = $disabled_inprogress_tip % 'Update'
#set $curUpdate = '%s>%s' % (('', $reason)[$curUpdate_disabled],
$option_state % ('update', 'update', ('', $disabled + $reason)[$curUpdate_disabled]))
#slurp
#set $curRefresh_disabled = $sickbeard.showQueueScheduler.action.isBeingRefreshed($curShow)\
or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow)
#set $reason = $disabled_inprogress_tip % 'Rescan'
#set $curRefresh = '%s>%s' % (('', $reason)[$curRefresh_disabled],
$option_state % ('refresh', 'refresh', ('', $disabled + $reason)[$curRefresh_disabled]))
#slurp
#set $curRename_disabled = $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow)\
or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow)
#set $reason = $disabled_inprogress_tip % 'Rename'
#set $curRename = '%s>%s' % (('', $reason)[$curRename_disabled],
$option_state % ('rename', 'rename', ('', $disabled + $reason)[$curRename_disabled]))
#slurp
#set $subtitles_disabled = not $curShow.subtitles\
or $sickbeard.showQueueScheduler.action.isBeingSubtitled($curShow)\
or $sickbeard.showQueueScheduler.action.isInSubtitleQueue($curShow)
#set $reason = ($disabled_inprogress_tip % 'Search subtitle', $disabled_subtitles_tip)[not $curShow.subtitles]
#set $curSubtitle = '%s>%s' % (('', $reason)[$subtitles_disabled],
$option_state % ('subtitle', 'subtitle', ('', $disabled + $reason)[$subtitles_disabled]))
#slurp
#set $curDelete_disabled = $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow)\
or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow)\
or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow)
#set $reason = $disabled_inprogress_tip % 'Rename or rescan'
#set $curDelete = '%s>%s' % (('', $reason)[$curDelete_disabled],
$option_state % ('delete', 'delete', ('', $disabled + $reason)[$curDelete_disabled]))
#slurp
#set $curRemove_disabled = $sickbeard.showQueueScheduler.action.isBeingRenamed($curShow)\
or $sickbeard.showQueueScheduler.action.isInRenameQueue($curShow)\
or $sickbeard.showQueueScheduler.action.isInRefreshQueue($curShow)
##set $reason = $disabled_inprogress_tip % 'Rename or rescan'
#set $curRemove = '%s>%s' % (('', $reason)[$curRemove_disabled],
$option_state % ('remove', 'remove', ('', $disabled + $reason)[$curRemove_disabled]))
<tr>
<td align="center"><input type="checkbox" class="editCheck" id="edit-$curShow.indexerid" /></td>
<td align="center"><input type="checkbox" class="editCheck" id="edit-$curShow.indexerid"></td>
<td class="tvShow"><a href="$sbRoot/home/displayShow?show=$curShow.indexerid">$curShow.name</a></td>
#if $curShow.quality in $qualityPresets:
#if $curShow.quality in $qualityPresets
<td align="center"><span class="quality $qualityPresetStrings[$curShow.quality]">$qualityPresetStrings[$curShow.quality]</span></td>
#else:
#else
<td align="center"><span class="quality Custom">Custom</span></td>
#end if
<td align="center"><img src="$sbRoot/images/#if int($curShow.is_sports) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
<td align="center"><img src="$sbRoot/images/#if int($curShow.is_scene) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
<td align="center"><img src="$sbRoot/images/#if int($curShow.is_anime) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
<td align="center"><img src="$sbRoot/images/#if int($curShow.flatten_folders) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
<td align="center"><img src="$sbRoot/images/#if int($curShow.paused) == 1 then "yes16.png\" alt=\"Y\"" else "no16.png\" alt=\"N\""# width="16" height="16" /></td>
#end if
#if $has_any_sports
<td align="center"><img src="$sbRoot/images/#if 1 == int($curShow.is_sports) then $yes else $no#" width="16" height="16" /></td>
#end if
<td align="center"><img src="$sbRoot/images/#if 1 == int($curShow.is_scene) then $yes else $no#" width="16" height="16" /></td>
#if $has_any_anime
<td align="center"><img src="$sbRoot/images/#if 1 == int($curShow.is_anime) then $yes else $no#" width="16" height="16" /></td>
#end if
#if $has_any_flat_folders
<td align="center"><img src="$sbRoot/images/#if 1 == int($curShow.flatten_folders) then $yes else $no#" width="16" height="16" /></td>
#end if
<td align="center"><img src="$sbRoot/images/#if 1 == int($curShow.paused) then $yes else $no#" width="16" height="16" /></td>
<td align="center">$curShow.status</td>
<td align="center">$curUpdate</td>
<td align="center">$curRefresh</td>
<td align="center">$curRename</td>
#if $sickbeard.USE_SUBTITLES:
<td align="center">$curSubtitle</td>
#end if
<td align="center">$curDelete</td>
<td align="center">$curRemove</td>
<td align="center"$curUpdate</td>
<td align="center"$curRefresh</td>
<td align="center"$curRename</td>
#if $sickbeard.USE_SUBTITLES
<td align="center"$curSubtitle</td>
#end if
<td align="center"$curDelete</td>
<td align="center"$curRemove</td>
</tr>
#end for
#end for
</tbody>
</table>
</form>
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -7,7 +7,7 @@
#set global $sbPath = '..'
#set global $topmenu = 'manage'#
#set global $topmenu = 'manage'
#import os.path
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
@ -29,7 +29,7 @@
dateHasTime : false,
dateFormat : '${sickbeard.DATE_PRESET}',
timeFormat : '${sickbeard.TIME_PRESET}',
trimZero : #if $sickbeard.TRIM_ZERO then "true" else "false"#
trimZero : #if $sickbeard.TRIM_ZERO then 'true' else 'false'#
});
#end if
@ -44,70 +44,77 @@
#else
<h1 class="title">$title</h1>
#end if
#set $totalWanted = 0
#set $totalQual = 0
#for $curShow in $sickbeard.showList:
#set $totalWanted = $totalWanted + $showCounts[$curShow.indexerid][$Overview.WANTED]
#set $totalQual = $totalQual + $showCounts[$curShow.indexerid][$Overview.QUAL]
#set $totalWanted += $showCounts[$curShow.indexerid][$Overview.WANTED]
#set $totalQual += $showCounts[$curShow.indexerid][$Overview.QUAL]
#end for
<div class="h2footer pull-right">
<span class="listing-key wanted">Wanted: <b>$totalWanted</b></span>
<span class="listing-key qual">Low Quality: <b>$totalQual</b></span>
</div><br/>
</div>
<br/>
<div class="pull-left">
Jump to Show
<select id="pickShow" class="form-control form-control-inline input-sm">
#for $curShow in sorted($sickbeard.showList, key = operator.attrgetter('name')):
#if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] != 0:
<option value="$curShow.indexerid">$curShow.name</option>
#end if
#end for
</select>
#for $curShow in sorted($sickbeard.showList, key = operator.attrgetter('name')):
#if 0 != $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED]:
<option value="$curShow.indexerid">$curShow.name</option>
#end if
#end for
</select>
</div>
<table class="sickbeardTable" cellspacing="0" border="0" cellpadding="0">
#for $curShow in sorted($sickbeard.showList, key = operator.attrgetter('name')):
#if $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED] == 0:
#continue
#end if
#if 0 == $showCounts[$curShow.indexerid][$Overview.QUAL] + $showCounts[$curShow.indexerid][$Overview.WANTED]:
#continue
#end if
<tr class="seasonheader" id="show-$curShow.indexerid">
<td colspan="3" class="align-left">
<br/><h2><a href="$sbRoot/home/displayShow?show=$curShow.indexerid">$curShow.name</a></h2>
<br/>
<h2><a href="$sbRoot/home/displayShow?show=$curShow.indexerid">$curShow.name</a></h2>
<div class="pull-right">
<span class="listing-key wanted">Wanted: <b>$showCounts[$curShow.indexerid][$Overview.WANTED]</b></span>
<span class="listing-key qual">Low Quality: <b>$showCounts[$curShow.indexerid][$Overview.QUAL]</b></span>
<a class="btn btn-inline forceBacklog" href="$sbRoot/manage/backlogShow?indexer_id=$curShow.indexerid"><i class="icon-play-circle icon-white"></i> Force Backlog</a>
#if not $curShow.paused:
<a class="btn btn-inline forceBacklog" href="$sbRoot/manage/backlogShow?indexer_id=$curShow.indexerid"><i class="sgicon-play"></i> Force Backlog</a>
#else
<span class="quality SD btn-inline forceBacklog" style="padding:4px 10px; margin-bottom:1px"><i class="sgicon-pause"></i> Paused</span>
#end if
</div>
</td>
</tr>
<tr class="seasoncols"><th>Episode</th><th>Name</th><th class="nowrap">Airdate</th></tr>
#for $curResult in $showSQLResults[$curShow.indexerid]:
#set $whichStr = $str($curResult['season']) + 'x' + $str($curResult['episode'])
#try:
#set $overview = $showCats[$curShow.indexerid][$whichStr]
#except Exception
#continue
#end try
#for $curResult in $showSQLResults[$curShow.indexerid]:
#set $whichStr = $str($curResult['season']) + 'x' + $str($curResult['episode'])
#try:
#set $overview = $showCats[$curShow.indexerid][$whichStr]
#except Exception
#continue
#end try
#if $overview not in ($Overview.QUAL, $Overview.WANTED):
#continue
#end if
#if $overview not in ($Overview.QUAL, $Overview.WANTED):
#continue
#end if
<tr class="seasonstyle $Overview.overviewStrings[$showCats[$curShow.indexerid][$whichStr]]">
<td class="tableleft" align="center">$whichStr</td>
<td>$curResult["name"]</td>
<td class="tableright" align="center" class="nowrap"><div class="${fuzzydate}">#if int($curResult['airdate']) == 1 then 'never' else $sbdatetime.sbdatetime.sbfdate($sbdatetime.sbdatetime.convert_to_setting($network_timezones.parse_date_time($curResult['airdate'],$curShow.airs,$curShow.network)))#</div></td>
</tr>
#end for
#end for
#end for
</table>

View file

@ -1,89 +1,112 @@
#import sickbeard
#import datetime
#from sickbeard import common
#set global $title="Episode Overview"
#set global $header="Episode Overview"
#set global $sbPath=".."
#set global $title = 'Episode Overview'
#set global $header = 'Episode Overview'
#set global $sbPath = '..'
#set global $topmenu = 'manage'
#set global $topmenu="manage"#
#import os.path
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
<div id="content960">
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#end if
#if not $whichStatus or ($whichStatus and not $ep_counts):
#if not $whichStatus or ($whichStatus and not $ep_counts)
#if $whichStatus:
<h2>None of your episodes have status $common.statusStrings[$int($whichStatus)]</h2>
<br />
#end if
#if $whichStatus:
<h3>no episodes have status <span class="grey-text">$common.statusStrings[$int($whichStatus)].lower()</span></h3>
#end if
<form action="$sbRoot/manage/episodeStatuses" method="get">
Manage episodes with status <select name="whichStatus" class="form-control form-control-inline input-sm">
#for $curStatus in [$common.SKIPPED, $common.SNATCHED, $common.WANTED, $common.ARCHIVED, $common.IGNORED]:
<option value="$curStatus">$common.statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline" type="submit" value="Manage" />
</form>
<form action="$sbRoot/manage/episodeStatuses" method="get">
Manage episodes with status
<select name="whichStatus" class="form-control form-control-inline input-sm" style="margin:0 10px">
#for $curStatus in [$common.SKIPPED, $common.UNKNOWN, $common.SNATCHED, $common.WANTED, $common.ARCHIVED, $common.IGNORED]:
<option value="$curStatus">$common.statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline" type="submit" value="Manage">
</form>
#else
#if $whichStatus in ($common.ARCHIVED, $common.IGNORED, $common.SNATCHED):
#set $row_class = 'good'
#else
#set $row_class = $common.Overview.overviewStrings[$whichStatus]
#end if
#set $statusList = [$common.SKIPPED, $common.ARCHIVED, $common.IGNORED]
#if $int($whichStatus) in $statusList
$statusList.remove($int($whichStatus))
#end if
#if $int($whichStatus) in [$common.SNATCHED, $common.SNATCHED_PROPER]
$statusList.append($common.FAILED)
#end if
<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?$sbPID"></script>
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus" />
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus">
<h2><span class="grey-text">Shows containing</span> $common.statusStrings[$int($whichStatus)] <span class="grey-text">episodes</span></h2>
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$common.statusStrings[$int($whichStatus)].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>
<br />
<input type="hidden" id="row_class" value="$row_class">
#if $whichStatus in ($common.ARCHIVED, $common.IGNORED, $common.SNATCHED):
#set $row_class = "good"
#else
#set $row_class = $common.Overview.overviewStrings[$whichStatus]
#end if
<input type="hidden" id="row_class" value="$row_class" />
<div class="form-group">
<span>Set checked shows/episodes to</span>
<select name="newStatus" class="form-control form-control-inline input-sm" style="margin:0 10px 0 5px">
#for $curStatus in $statusList:
<option value="$curStatus">$common.statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline go" type="submit" value="Go">
Set checked shows/episodes to <select name="newStatus" class="form-control form-control-inline input-sm">
#set $statusList = [$common.SKIPPED, $common.WANTED, $common.ARCHIVED, $common.IGNORED]
#if $int($whichStatus) in $statusList
$statusList.remove($int($whichStatus))
#end if
<span class="red-text" style="margin:0 0 0 30px">Override checked status to</span>
<select name="wantedStatus" class="form-control form-control-inline input-sm" style="margin:0 10px 0 5px">
<option value="$common.UNKNOWN">nothing</option>
<option value="$common.WANTED">$common.statusStrings[$common.WANTED]</option>
</select>
<input class="btn btn-inline go" type="submit" value="Go">
</div>
#if $int($whichStatus) in [$common.SNATCHED, $common.SNATCHED_PROPER]
$statusList.append($common.FAILED)
#end if
<div class="form-group">
<input type="button" class="btn btn-xs selectAllShows" value="Select all">
<input type="button" class="btn btn-xs unselectAllShows" value="Clear all">
<input type="button" class="btn btn-xs expandAll" value="Expand All Shows">
</div>
#for $curStatus in $statusList:
<option value="$curStatus">$common.statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline go" type="submit" value="Go" />
<div>
<button type="button" class="btn btn-xs selectAllShows">Select all</a></button>
<button type="button" class="btn btn-xs unselectAllShows">Clear all</a></button>
</div>
<br />
<table class="sickbeardTable manageTable" cellspacing="1" border="0" cellpadding="0">
#for $cur_indexer_id in $sorted_show_ids:
<tr id="$cur_indexer_id">
<th><input type="checkbox" class="allCheck" id="allCheck-$cur_indexer_id" name="$cur_indexer_id-all" /></th>
<th colspan="2" style="width: 100%; text-align: left;"><a class="whitelink" href="$sbRoot/home/displayShow?show=$cur_indexer_id">$show_names[$cur_indexer_id]</a> ($ep_counts[$cur_indexer_id]) <input type="button" class="pull-right get_more_eps btn" id="$cur_indexer_id" value="Expand" /></th>
</tr>
#end for
</table>
</form>
<table class="sickbeardTable manageTable" cellspacing="1" border="0" cellpadding="0">
<thead></thead>
<tbody>
#for $cur_indexer_id in $sorted_show_ids:
#if 0 == int($never_counts[$cur_indexer_id])
#set $output = '%d' % $ep_counts[$cur_indexer_id]
#elif $ep_counts[$cur_indexer_id] != $never_counts[$cur_indexer_id]
#set $diff = $ep_counts[$cur_indexer_id] - $never_counts[$cur_indexer_id]
#set $output = '%d' % $diff + ('', (' episode%s plus %s never with an airdate' % (('s', '')[1 == $ep_counts[$cur_indexer_id]], $never_counts[$cur_indexer_id])))[0 < $never_counts[$cur_indexer_id]]
#else
#set $output = '%s never with an airdate' % (('all %s %ss', '%s %s')[1 == $ep_counts[$cur_indexer_id]] % ($ep_counts[$cur_indexer_id], 'episode'))
#end if
<tr id="$cur_indexer_id" class="header">
<td><input type="checkbox" class="allCheck" id="allCheck-$cur_indexer_id" name="$cur_indexer_id-all"></td>
<td colspan="2" style="width:100%;text-align:left">
<a class="whitelink" href="$sbRoot/home/displayShow?show=$cur_indexer_id">$show_names[$cur_indexer_id]</a> <span style="color:#999">($output)</span><input type="button" class="pull-right get_more_eps btn" id="$cur_indexer_id-more" value="Expand"><input type="button" class="pull-right get_less_eps btn" id="$cur_indexer_id-less" value="Collapse">
</td>
</tr>
#end for
</tbody>
</table>
</form>
#end if
</div>
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -19,8 +19,8 @@
<div id="summary2" class="align-left">
<h3>Backlog Search:</h3>
<a class="btn" href="$sbRoot/manage/manageSearches/forceBacklog"><i class="icon-exclamation-sign"></i> Force</a>
<a class="btn" href="$sbRoot/manage/manageSearches/pauseBacklog?paused=#if $backlogPaused then "0" else "1"#"><i class="#if $backlogPaused then "icon-play" else "icon-pause"#"></i> #if $backlogPaused then "Unpause" else "Pause"#</a>
<a class="btn" href="$sbRoot/manage/manageSearches/forceBacklog"><i class="sgicon-play"></i> Force</a>
<a class="btn" href="$sbRoot/manage/manageSearches/pauseBacklog?paused=#if $backlogPaused then "0" else "1"#"><i class="#if $backlogPaused then "sgicon-play" else "sgicon-pause"#"></i> #if $backlogPaused then "Unpause" else "Pause"#</a>
#if not $backlogRunning:
Not in progress<br />
#else:
@ -30,7 +30,7 @@ Currently running<br />
<br />
<h3>Recent Search:</h3>
<a class="btn" href="$sbRoot/manage/manageSearches/forceSearch"><i class="icon-exclamation-sign"></i> Force</a>
<a class="btn" href="$sbRoot/manage/manageSearches/forceSearch"><i class="sgicon-play"></i> Force</a>
#if not $recentSearchStatus:
Not in progress<br />
#else:
@ -39,7 +39,7 @@ In Progress<br />
<br />
<h3>Find Propers Search:</h3>
<a class="btn" href="$sbRoot/manage/manageSearches/forceFindPropers"><i class="icon-exclamation-sign"></i> Force</a>
<a class="btn" href="$sbRoot/manage/manageSearches/forceFindPropers"><i class="sgicon-play"></i> Force</a>
#if not $findPropersStatus:
Not in progress<br />
#else:
@ -48,7 +48,7 @@ In Progress<br />
<br />
<h3>Version Check:</h3>
<a class="btn" href="$sbRoot/manage/manageSearches/forceVersionCheck"><i class="icon-check"></i> Force Check</a>
<a class="btn" href="$sbRoot/manage/manageSearches/forceVersionCheck"><i class="sgicon-updatecheck"></i> Force Check</a>
<br /> <br />
<h3>Search Queue:</h3>

View file

@ -1,187 +1,184 @@
#import sickbeard
#from sickbeard import common
#from sickbeard import exceptions
#set global $title="Mass Edit"
#set global $header="Mass Edit"
#set global $title = 'Mass Edit'
#set global $header = 'Mass Edit'
#set global $sbPath=".."
#set global $sbPath = '..'
#set global $topmenu="manage"#
#set global $topmenu = 'manage'#
#import os.path
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
#if $quality_value != None:
#set $initial_quality = int($quality_value)
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
#if None is not $quality_value:
#set $initial_quality = int($quality_value)
#else:
#set $initial_quality = $common.SD
#set $initial_quality = $common.SD
#end if
#set $anyQualities, $bestQualities = $common.Quality.splitQuality($initial_quality)
<script type="text/javascript" src="$sbRoot/js/qualityChooser.js?$sbPID"></script>
<script type="text/javascript" src="$sbRoot/js/massEdit.js?$sbPID"></script>
<form action="massEditSubmit" method="post">
<input type="hidden" name="toEdit" value="$showList" />
<input type="hidden" name="toEdit" value="$showList">
<div class="optionWrapper">
<span class="selectTitle">Root Directories <span class="separator">*</span></span><br />
#for $cur_dir in $root_dir_list:
#set $cur_index = $root_dir_list.index($cur_dir)
<div>
<input class="btn edit_root_dir" type="button" class="edit_root_dir" id="edit_root_dir_$cur_index" value="Edit" />
<input class="btn delete_root_dir" type="button" class="delete_root_dir" id="delete_root_dir_$cur_index" value="Delete" />
$cur_dir => <span id="display_new_root_dir_$cur_index">$cur_dir</span>
</div>
<input type="hidden" name="orig_root_dir_$cur_index" value="$cur_dir" />
<input type="text" style="display: none" name="new_root_dir_$cur_index" id="new_root_dir_$cur_index" class="new_root_dir" value="$cur_dir" />
#end for
<div class="optionWrapper">
<span class="selectTitle">Parent folder <span class="separator">*</span></span><br />
#set $selected = 'selected="selected"'
#for $cur_dir in $root_dir_list:
#set $cur_index = $root_dir_list.index($cur_dir)
<div>
<input class="btn edit_root_dir" type="button" class="edit_root_dir" id="edit_root_dir_$cur_index" value="Edit">
<input class="btn delete_root_dir" type="button" class="delete_root_dir" id="delete_root_dir_$cur_index" value="Delete">
$cur_dir => <span id="display_new_root_dir_$cur_index">$cur_dir</span>
</div>
<input type="hidden" name="orig_root_dir_$cur_index" value="$cur_dir">
<input type="text" style="display:none" name="new_root_dir_$cur_index" id="new_root_dir_$cur_index" class="new_root_dir" value="$cur_dir">
#end for
</div>
</div>
<div class="optionWrapper">
<span class="selectTitle">Paused</span>
<div class="selectChoices">
<select id="edit_paused" name="paused" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $paused_value then $selected else ''#>enable</option>
<option value="disable" #if $paused_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Quality</span>
<div class="selectChoices">
<select id="qualityPreset" name="quality_preset" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
#set $selected = None
<option value="0" #if $quality_value != None and $quality_value not in $common.qualityPresets then "selected=\"selected\"" else ""#>Custom</option>
#for $curPreset in sorted($common.qualityPresets):
<option value="$curPreset" #if $quality_value == $curPreset then "selected=\"selected\"" else ""#>$common.qualityPresetStrings[$curPreset]</option>
#end for
</select>
</div><br />
<div class="optionWrapper">
<span class="selectTitle">Quality</span>
<div class="selectChoices">
<select id="qualityPreset" name="quality_preset" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="0" #if None is not $quality_value and $quality_value not in $common.qualityPresets then $selected else ''#>Custom</option>
#for $curPreset in sorted($common.qualityPresets):
<option value="$curPreset" #if $quality_value == $curPreset then $selected else ''#>$common.qualityPresetStrings[$curPreset]</option>
#end for
</select>
</div><br />
<div id="customQuality">
<div class="manageCustom pull-left">
<h4>Inital</h4>
#set $anyQualityList = filter(lambda x: x > $common.Quality.NONE, $common.Quality.qualityStrings)
<select id="anyQualities" name="anyQualities" multiple="multiple" size="len($anyQualityList)">
#for $curQuality in sorted($anyQualityList):
<option value="$curQuality" #if $curQuality in $anyQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option>
#end for
</select>
</div>
<div class="manageCustom pull-left">
<h4>Archive</h4>
#set $bestQualityList = filter(lambda x: x > $common.Quality.SDTV, $common.Quality.qualityStrings)
<select id="bestQualities" name="bestQualities" multiple="multiple" size="len($bestQualityList)">
#for $curQuality in sorted($bestQualityList):
<option value="$curQuality" #if $curQuality in $bestQualities then "selected=\"selected\"" else ""#>$common.Quality.qualityStrings[$curQuality]</option>
#end for
</select>
</div>
<br />
</div>
</div>
<div id="customQuality">
<div class="manageCustom pull-left">
<h4>Inital</h4>
#set $anyQualityList = filter(lambda x: x > $common.Quality.NONE, $common.Quality.qualityStrings)
<select id="anyQualities" name="anyQualities" multiple="multiple" size="len($anyQualityList)">
#for $curQuality in sorted($anyQualityList):
<option value="$curQuality" #if $curQuality in $anyQualities then $selected else ''#>$common.Quality.qualityStrings[$curQuality]</option>
#end for
</select>
</div>
<div class="manageCustom pull-left">
<h4>Archive</h4>
#set $bestQualityList = filter(lambda x: x > $common.Quality.SDTV, $common.Quality.qualityStrings)
<select id="bestQualities" name="bestQualities" multiple="multiple" size="len($bestQualityList)">
#for $curQuality in sorted($bestQualityList):
<option value="$curQuality" #if $curQuality in $bestQualities then $selected else ''#>$common.Quality.qualityStrings[$curQuality]</option>
#end for
</select>
</div><br />
</div>
</div>
#if $anyQualities + $bestQualities:
#set $isSelected = ' selected="selected"'
#set $isEnabled = $isSelected
#set $isDisabled = $isSelected
#if $archive_firstmatch_value##set $isDisabled = ''##else##set $isEnabled = ''##end if#
<div class="optionWrapper clearfix">
<span class="selectTitle">Archive on first match</span>
<div class="selectChoices">
<select id="edit_archive_firstmatch" name="archive_firstmatch" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable"${isEnabled}>enable</option>
<option value="disable"${isDisabled}>disable</option>
</select>
</div>
</div>
#set $isSelected = ' selected="selected"'
#set $isEnabled = $isSelected
#set $isDisabled = $isSelected
#if $archive_firstmatch_value##set $isDisabled = ''##else##set $isEnabled = ''##end if#
<div class="optionWrapper clearfix">
<span class="selectTitle">Archive on first match</span>
<div class="selectChoices">
<select id="edit_archive_firstmatch" name="archive_firstmatch" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable"${isEnabled}>enable</option>
<option value="disable"${isDisabled}>disable</option>
</select>
</div>
</div>
#end if
<div class="optionWrapper clearfix">
<span class="selectTitle">Flatten Folders <span class="separator">*</span></span>
<div class="selectChoices">
<select id="edit_flatten_folders" name="flatten_folders" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $flatten_folders_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $flatten_folders_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div>
</div>
<div class="optionWrapper clearfix">
<span class="selectTitle">Flat folder structure <span class="separator">*</span></span>
<div class="selectChoices">
<select id="edit_flatten_folders" name="flatten_folders" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $flatten_folders_value then $selected else ''#>enable</option>
<option value="disable" #if $flatten_folders_value == False then $selected else ''#>disable</option>
</select>
</div>
</div>
<div class="optionWrapper">
<span class="selectTitle">Paused</span>
<div class="selectChoices">
<select id="edit_paused" name="paused" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $paused_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $paused_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Air by date episode names</span>
<div class="selectChoices">
<select id="edit_air_by_date" name="air_by_date" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $air_by_date_value then $selected else ''#>enable</option>
<option value="disable" #if $air_by_date_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Scene Numbering</span>
<div class="selectChoices">
<select id="edit_scene" name="scene" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $scene_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $scene_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Scene numbering</span>
<div class="selectChoices">
<select id="edit_scene" name="scene" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $scene_value then $selected else ''#>enable</option>
<option value="disable" #if $scene_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Anime</span>
<div class="selectChoices">
<select id="edit_anime" name="anime" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $anime_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $anime_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Subtitles<span class="separator"></span></span>
<div class="selectChoices">
<select id="edit_subtitles" name="subtitles" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $subtitles_value then $selected else ''#>enable</option>
<option value="disable" #if $subtitles_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Sports</span>
<div class="selectChoices">
<select id="edit_sports" name="sports" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $sports_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $sports_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Show is sports</span>
<div class="selectChoices">
<select id="edit_sports" name="sports" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $sports_value then $selected else ''#>enable</option>
<option value="disable" #if $sports_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Air-By-Date</span>
<div class="selectChoices">
<select id="edit_air_by_date" name="air_by_date" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $air_by_date_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $air_by_date_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Show is anime</span>
<div class="selectChoices">
<select id="edit_anime" name="anime" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $anime_value then $selected else ''#>enable</option>
<option value="disable" #if $anime_value == False then $selected else ''#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<span class="selectTitle">Subtitles<span class="separator"></span></span>
<div class="selectChoices">
<select id="edit_subtitles" name="subtitles" class="form-control form-control-inline input-sm">
<option value="keep">&lt; keep &gt;</option>
<option value="enable" #if $subtitles_value then "selected=\"selected\"" else ""#>enable</option>
<option value="disable" #if $subtitles_value == False then "selected=\"selected\"" else ""#>disable</option>
</select>
</div><br />
</div>
<div class="optionWrapper">
<br /><span class="separator" style="font-size: 1.2em; font-weight: 700;">*</span>
Changing these settings will cause the selected shows to be refreshed.
</div>
<div class="optionWrapper" style="text-align: center;">
<input type="submit" value="Submit" class="btn" /><br />
</div>
<div class="optionWrapper" style="font-size:13px;margin-top:15px">
<span class="separator" style="font-size:1.2em; font-weight:700">*</span>
Changing these settings will cause selected shows to be refreshed
</div>
<div class="optionWrapper" style="text-align:center">
<input type="submit" value="Submit" class="btn"><br />
</div>
</form>
<br />
<script type="text/javascript" charset="utf-8">
<!--
jQuery('#location').fileBrowser({ title: 'Select Show Location' });
jQuery('#location').fileBrowser({ title:'Select Show Location' });
//-->
</script>
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -1,35 +1,39 @@
$(document).ready(function () {
$(document).ready(function(){
$('#saveDefaultsButton').click(function () {
var anyQualArray = [];
var bestQualArray = [];
$('#anyQualities option:selected').each(function (i, d) {
anyQualArray.push($(d).val());
});
$('#bestQualities option:selected').each(function (i, d) {
bestQualArray.push($(d).val());
});
$('#saveDefaultsButton').click(function() {
var anyQualArray = [], bestQualArray = [];
$.get(sbRoot + '/config/general/saveAddShowDefaults', {
defaultStatus: $('#statusSelect').val(),
anyQualities: anyQualArray.join(','),
bestQualities: bestQualArray.join(','),
defaultFlattenFolders: $('#flatten_folders').prop('checked'),
subtitles: $('#subtitles').prop('checked'),
anime: $('#anime').prop('checked'),
scene: $('#scene').prop('checked')
});
$('#anyQualities option:selected').each(function(i, d) {
anyQualArray.push($(d).val());
});
$('#bestQualities option:selected').each(function(i, d) {
bestQualArray.push($(d).val());
});
$(this).attr('disabled', true);
new PNotify({
title: 'Saved Defaults',
text: 'Your "add show" defaults have been set to your current selections.',
shadow: false
});
});
$.get(sbRoot + '/config/general/saveAddShowDefaults', {
default_status: $('#statusSelect').val(),
any_qualities: anyQualArray.join(','),
best_qualities: bestQualArray.join(','),
default_wanted_begin: $('#wanted_begin').val(),
default_wanted_latest: $('#wanted_latest').val(),
default_flatten_folders: $('#flatten_folders').prop('checked'),
default_scene: $('#scene').prop('checked'),
default_subtitles: $('#subtitles').prop('checked'),
default_anime: $('#anime').prop('checked')
});
$('#statusSelect, #qualityPreset, #flatten_folders, #anyQualities, #bestQualities, #subtitles, #scene, #anime').change(function () {
$('#saveDefaultsButton').attr('disabled', false);
});
new PNotify({
title: 'Saving Defaults',
text: 'Saving your "add show" defaults.',
shadow: false
});
$(this).attr('disabled', true);
});
$('#statusSelect, #qualityPreset, #anyQualities, #bestQualities, #wanted_begin, #wanted_latest,'
+ ' #flatten_folders, #scene, #subtitles, #anime').change(function() {
$('#saveDefaultsButton').attr('disabled', false);
});
});

View file

@ -1,13 +1,13 @@
$(document).ready(function(){
$(document).ready(function () {
var enabler = $('.enabler'),
viewIf = $('.viewIf');
enabler.each(function(){
enabler.each(function () {
if (!$(this).prop('checked'))
$('#content_' + $(this).attr('id')).hide();
});
enabler.click(function(){
enabler.click(function () {
var content_id = $('#content_' + $(this).attr('id'));
if ($(this).prop('checked'))
content_id.fadeIn('fast', 'linear');
@ -15,11 +15,11 @@ $(document).ready(function(){
content_id.fadeOut('fast', 'linear');
});
viewIf.each(function(){
viewIf.each(function () {
$(($(this).prop('checked') ? '.hide_if_' : '.show_if_') + $(this).attr('id')).hide();
});
viewIf.click(function(){
viewIf.click(function () {
var if_id = '_if_' + $(this).attr('id');
if ($(this).prop('checked')) {
$('.hide' + if_id).fadeOut('fast', 'linear');
@ -30,14 +30,14 @@ $(document).ready(function(){
}
});
var ui_update_trim_zero = (function() {
var ui_update_trim_zero = (function () {
var secs = ('00' + new Date().getSeconds().toString()).slice(-2),
elSecs = $('#trim_info_seconds'),
elTrimZero = $('#trim_zero');
elTrimZero.each(function() {
elTrimZero.each(function () {
var checked = $(this).prop('checked') && $('#fuzzy_dating').prop('checked');
$('#time_presets').find('option').each(function() {
$('#time_presets').find('option').each(function () {
var text = ($(this).text());
$(this).text(checked
? text.replace(/(\b\d+:\d\d):\d+/mg, '$1')
@ -54,13 +54,13 @@ $(document).ready(function(){
elSecs.fadeIn('fast', 'linear');
});
$('#trim_zero, #fuzzy_dating').click(function() {
$('#trim_zero, #fuzzy_dating').click(function () {
ui_update_trim_zero();
});
ui_update_trim_zero();
$('.datePresets').click(function(){
$('.datePresets').click(function () {
var elDatePresets = $('#date_presets'),
defaultPreset = elDatePresets.val();
if ($(this).prop('checked') && '%x' == defaultPreset) {
@ -86,49 +86,113 @@ $(document).ready(function(){
// bind 'myForm' and provide a simple callback function
$('#configForm').ajaxForm({
beforeSubmit: function(){
$('.config_submitter').each(function(){
beforeSubmit: function () {
$('.config_submitter').each(function () {
$(this).attr('disabled', 'disabled');
$(this).after('<span><img src="' + sbRoot + '/images/loading16' + themeSpinner + '.gif"> Saving...</span>');
$(this).hide();
});
$('.show_update_hour_value').text($('#show_update_hour').val())
},
success: function(response){
setTimeout(function(){config_success(response)}, 2000);
success: function (response) {
setTimeout(function () {config_success(response)}, 2000);
}
});
$('#api_key').click(function(){ $('#api_key').select() });
$('#generate_new_apikey').click(function(){
$('#api_key').click(function () {$('#api_key').select()});
$('#generate_new_apikey').click(function () {
$.get(sbRoot + '/config/general/generateKey',
function(data){
function (data) {
if (data.error != undefined) {
alert(data.error);
return;
}
$('#api_key').val(data);
});
});
});
$('#branchCheckout').click(function(){
$('#branchCheckout').click(function () {
window.location.href = sbRoot + '/home/branchCheckout?branch=' + $('#branchVersion').val();
});
$('#pullRequestCheckout').click(function(){
$('#pullRequestCheckout').click(function () {
window.location.href = sbRoot + '/home/pullRequestCheckout?branch=' + $('#pullRequestVersion').val();
});
fetch_branches();
fetch_pullrequests();
});
function config_success(response){
if (response == 'reload'){
function config_success(response) {
if (response == 'reload') {
window.location.reload(true);
}
$('.config_submitter').each(function(){
$('.config_submitter').each(function () {
$(this).removeAttr('disabled');
$(this).next().remove();
$(this).show();
});
$('#email_show').trigger('notify');
}
function fetch_pullrequests() {
$.getJSON(sbRoot + '/config/general/fetch_pullrequests', function (data) {
$('#pullRequestVersion').find('option').remove();
if (data['result'] == 'success') {
var pulls = [];
$.each(data['pulls'], function (i, pull) {
if (pull[0] != '') {
pulls.push(pull);
}
});
if (pulls.length > 0) {
$.each(pulls, function (i, text) {
add_option_to_pulls(text);
});
$('#pullRequestCheckout').removeAttr('disabled');
} else {
add_option_to_pulls(['No pull requests available', '']);
}
} else {
add_option_to_pulls(['Failed to connect to github', '']);
}
});
}
function fetch_branches() {
$.getJSON(sbRoot + '/config/general/fetch_branches', function (data) {
$('#branchVersion').find('option').remove();
if (data['result'] == 'success') {
var branches = [];
$.each(data['branches'], function (i, branch) {
if (branch != '') {
branches.push(branch);
}
});
if (branches.length > 0) {
$.each(branches, function (i, text) {
add_option_to_branches(text);
});
$('#branchCheckout').removeAttr('disabled');
} else {
add_option_to_branches('No branches available');
}
} else {
add_option_to_branches('Failed to connect to github');
}
});
}
function add_option_to_pulls(text) {
option = $('<option>');
option.attr('value', text[1]);
option.html(text[0]);
option.appendTo('#pullRequestVersion');
}
function add_option_to_branches(text) {
option = $('<option>');
option.attr('value', text);
option.html(text);
option.appendTo('#branchVersion');
}

View file

@ -56,6 +56,25 @@ $(document).ready(function(){
});
});
$('#testKODI').click(function () {
var kodi_host = $.trim($('#kodi_host').val());
var kodi_username = $.trim($('#kodi_username').val());
var kodi_password = $.trim($('#kodi_password').val());
if (!kodi_host) {
$('#testKODI-result').html('Please fill out the necessary fields above.');
$('#kodi_host').addClass('warning');
return;
}
$('#kodi_host').removeClass('warning');
$(this).prop('disabled', true);
$('#testKODI-result').html(loading);
$.get(sbRoot + '/home/testKODI', {'host': kodi_host, 'username': kodi_username, 'password': kodi_password})
.done(function (data) {
$('#testKODI-result').html(data);
$('#testKODI').prop('disabled', false);
});
});
$('#testPMC').click(function () {
var plex_host = $.trim($('#plex_host').val());
var plex_username = $.trim($('#plex_username').val());
@ -113,8 +132,11 @@ $(document).ready(function(){
});
$('#testPushover').click(function () {
var pushover_userkey = $('#pushover_userkey').val();
var pushover_apikey = $('#pushover_apikey').val();
var pushover_userkey = $.trim($('#pushover_userkey').val());
var pushover_apikey = $.trim($('#pushover_apikey').val());
var pushover_priority = $("#pushover_priority").val();
var pushover_device = $("#pushover_device").val();
var pushover_sound = $("#pushover_sound").val();
if (!pushover_userkey || !pushover_apikey) {
$('#testPushover-result').html('Please fill out the necessary fields above.');
if (!pushover_userkey) {
@ -132,13 +154,71 @@ $(document).ready(function(){
$('#pushover_userkey,#pushover_apikey').removeClass('warning');
$(this).prop('disabled', true);
$('#testPushover-result').html(loading);
$.get(sbRoot + '/home/testPushover', {'userKey': pushover_userkey, 'apiKey': pushover_apikey})
$.get(sbRoot + '/home/testPushover', {'userKey': pushover_userkey, 'apiKey': pushover_apikey, 'priority': pushover_priority, 'device': pushover_device, 'sound': pushover_sound})
.done(function (data) {
$('#testPushover-result').html(data);
$('#testPushover').prop('disabled', false);
});
});
function get_pushover_devices (msg) {
var pushover_userkey = $.trim($('#pushover_userkey').val());
var pushover_apikey = $.trim($('#pushover_apikey').val());
if (!pushover_userkey || !pushover_apikey) {
$('#testPushover-result').html('Please fill out the necessary fields above.');
if (!pushover_userkey) {
$('#pushover_userkey').addClass('warning');
} else {
$('#pushover_userkey').removeClass('warning');
}
if (!pushover_apikey) {
$('#pushover_apikey').addClass('warning');
} else {
$('#pushover_apikey').removeClass('warning');
}
return;
}
$(this).prop('disabled', true);
if (msg) {
$('#testPushover-result').html(loading);
}
var current_pushover_device = $('#pushover_device').val();
$.get(sbRoot + "/home/getPushoverDevices", {'userKey': pushover_userkey, 'apiKey': pushover_apikey})
.done(function (data) {
var devices = jQuery.parseJSON(data || '{}').devices;
$('#pushover_device_list').html('');
// add default option to send to all devices
$('#pushover_device_list').append('<option value="all" selected="selected">-- All Devices --</option>');
if (devices) {
for (var i = 0; i < devices.length; i++) {
// if a device in the list matches our current iden, select it
if (current_pushover_device == devices[i]) {
$('#pushover_device_list').append('<option value="' + devices[i] + '" selected="selected">' + devices[i] + '</option>');
} else {
$('#pushover_device_list').append('<option value="' + devices[i] + '">' + devices[i] + '</option>');
}
}
}
$('#getPushoverDevices').prop('disabled', false);
if (msg) {
$('#testPushover-result').html(msg);
}
});
$('#pushover_device_list').change(function () {
$('#pushover_device').val($('#pushover_device_list').val());
$('#testPushover-result').html('Don\'t forget to save your new Pushover settings.');
});
}
$('#getPushoverDevices').click(function () {
get_pushover_devices('Device list updated. Select specific device to use.');
});
if ($('#use_pushover').prop('checked')) {
get_pushover_devices();
}
$('#testLibnotify').click(function() {
$('#testLibnotify-result').html(loading);
$.get(sbRoot + '/home/testLibnotify',
@ -377,68 +457,73 @@ $(document).ready(function(){
});
$('#testPushbullet').click(function () {
var pushbullet_api = $.trim($('#pushbullet_api').val());
if (!pushbullet_api) {
var pushbullet_access_token = $.trim($('#pushbullet_access_token').val());
var pushbullet_device_iden = $('#pushbullet_device_iden').val();
if (!pushbullet_access_token) {
$('#testPushbullet-result').html('Please fill out the necessary fields above.');
$('#pushbullet_api').addClass('warning');
$('#pushbullet_access_token').addClass('warning');
return;
}
$('#pushbullet_api').removeClass('warning');
$('#pushbullet_access_token').removeClass('warning');
$(this).prop('disabled', true);
$('#testPushbullet-result').html(loading);
$.get(sbRoot + '/home/testPushbullet', {'api': pushbullet_api})
$.get(sbRoot + '/home/testPushbullet', {'accessToken': pushbullet_access_token, 'device_iden': pushbullet_device_iden})
.done(function (data) {
$('#testPushbullet-result').html(data);
$('#testPushbullet').prop('disabled', false);
});
});
function get_pushbullet_devices(msg){
if(msg){
function get_pushbullet_devices (msg) {
var pushbullet_access_token = $.trim($('#pushbullet_access_token').val());
if (!pushbullet_access_token) {
$('#testPushbullet-result').html('Please fill out the necessary fields above.');
$('#pushbullet_access_token').addClass('warning');
return;
}
$(this).prop("disabled", true);
if (msg) {
$('#testPushbullet-result').html(loading);
}
var pushbullet_api = $("#pushbullet_api").val();
if(!pushbullet_api) {
$('#testPushbullet-result').html("You didn't supply a Pushbullet api key");
$("#pushbullet_api").focus();
return false;
}
var current_pushbullet_device = $("#pushbullet_device").val();
$.get(sbRoot + "/home/getPushbulletDevices", {'api': pushbullet_api},
function (data) {
var devices = jQuery.parseJSON(data).devices;
$("#pushbullet_device_list").html('');
for (var i = 0; i < devices.length; i++) {
if(devices[i].active == true) {
if(current_pushbullet_device == devices[i].iden) {
$("#pushbullet_device_list").append('<option value="'+devices[i].iden+'" selected>' + devices[i].nickname + '</option>')
} else {
$("#pushbullet_device_list").append('<option value="'+devices[i].iden+'">' + devices[i].nickname + '</option>')
var current_pushbullet_device = $('#pushbullet_device_iden').val();
$.get(sbRoot + '/home/getPushbulletDevices', {'accessToken': pushbullet_access_token})
.done(function (data) {
var devices = jQuery.parseJSON(data || '{}').devices;
$('#pushbullet_device_list').html('');
// add default option to send to all devices
$('#pushbullet_device_list').append('<option value="" selected="selected">-- All Devices --</option>');
if (devices) {
for (var i = 0; i < devices.length; i++) {
// only list active device targets
if (devices[i].active == true) {
// if a device in the list matches our current iden, select it
if (current_pushbullet_device == devices[i].iden) {
$('#pushbullet_device_list').append('<option value="' + devices[i].iden + '" selected="selected">' + devices[i].manufacturer + ' ' + devices[i].nickname + '</option>');
} else {
$('#pushbullet_device_list').append('<option value="' + devices[i].iden + '">' + devices[i].manufacturer + ' ' + devices[i].nickname + '</option>');
}
}
}
}
if(msg) {
$('#getPushbulletDevices').prop('disabled', false);
if (msg) {
$('#testPushbullet-result').html(msg);
}
}
);
});
$("#pushbullet_device_list").change(function(){
$("#pushbullet_device").val($("#pushbullet_device_list").val());
$('#testPushbullet-result').html("Don't forget to save your new pushbullet settings.");
$('#pushbullet_device_list').change(function () {
$('#pushbullet_device_iden').val($('#pushbullet_device_list').val());
$('#testPushbullet-result').html('Don\'t forget to save your new Pushbullet settings.');
});
};
}
$('#getPushbulletDevices').click(function(){
get_pushbullet_devices("Device list updated. Please choose a device to push to.");
$('#getPushbulletDevices').click(function () {
get_pushbullet_devices('Device list updated. Select specific device to use.');
});
// we have to call this function on dom ready to create the devices select
get_pushbullet_devices();
if ($('#use_pushbullet').prop('checked')) {
get_pushbullet_devices();
}
$('#email_show').change(function () {
var key = parseInt($('#email_show').val(), 10);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
/* jQuery - Collapser - Plugin v2.0 www.aakashweb.com (c) 2014 Aakash Chakravarthy MIT License. */
(function(e,m,p,q){function l(b,f){this.o=e.extend({},n,f);this.e=e(b);this.init()}var n={target:"next",mode:"words",speed:"slow",truncate:10,ellipsis:"...",effect:"fade",controlBtn:"",showText:"Show more",hideText:"Hide text",showClass:"show-class",hideClass:"hide-class",atStart:"hide",lockHide:!1,dynamic:!1,changeText:!1,beforeShow:null,afterShow:null,beforeHide:null,afterHide:null};l.prototype={init:function(){var b=this;b.mode=b.o.mode;b.remaining={};b.ctrlHtml=' <a href="#" data-ctrl class="'+
(e.isFunction(b.o.controlBtn)?"":b.o.controlBtn)+'"></a>';e(b.e).each(function(){e(this).data("oCnt",b.e.html());var a=e.isFunction(b.o.atStart)?b.o.atStart.call(b.e):b.o.atStart,a="undefined"!==typeof b.e.attr("data-start")?b.e.attr("data-start"):a;"hide"==a?b.hide(b.e,0):b.show(b.e,0)});var f;e(m).on("resize",function(){b.o.dynamic&&"lines"==b.mode&&(clearTimeout(f),f=setTimeout(function(){b.reInit(b.e)},100))})},show:function(b,f){var a=this,c=e(b);"undefined"===typeof f&&(f=a.o.speed);var g=function(){e.isFunction(a.o.afterShow)&&
a.o.afterShow.call(a.e,a)};e.isFunction(a.o.beforeShow)&&a.o.beforeShow.call(a.e,a);switch(a.mode){case "chars":case "words":var d=c.height();c.html(c.data("tHTML"));var h=c.height();c.height(d);c.animate({height:h},f,function(){c.height("auto");g()}).removeClass(a.o.hideClass).addClass(a.o.showClass);c.data("tHTML",c.html());break;case "lines":0==c.children("div").length&&c.wrapInner("<div>");var k=c.children("div"),d=k.height(),h=k.html(c.data("oCnt")).css("height","").height();k.css("height",d);
k.animate({height:h},f,function(){k.height("auto");g()});c.removeClass(a.o.hideClass).addClass(a.o.showClass);break;case "block":a.blockMode(c,"show",f,g)}a.status=1;if(!0==a.o.lockHide)return c.find("[data-ctrl]").remove(),"";if("block"==a.mode)c.off("click.coll").on("click.coll",function(b){b.preventDefault();a.hide(c)});else 0!=c.find("[data-ctrl]").length||e.isFunction(a.o.controlBtn)||c.append(a.ctrlHtml),a.ctrlBtn=e.isFunction(a.o.controlBtn)?a.o.controlBtn.call(a.e):e(c.find("[data-ctrl]")),
a.ctrlBtn.off("click.coll").on("click.coll",function(b){b.preventDefault();a.hide(c)}).html(a.o.hideText)},hide:function(b,f){var a=this,c=e(b);"undefined"===typeof f&&(f=a.o.speed);var g=function(){e.isFunction(a.o.afterHide)&&a.o.afterHide.call(a.e,a)};e.isFunction(a.o.beforeHide)&&a.o.beforeHide.call(a.e,a);c.find("[data-ctrl]").remove();switch(a.mode){case "chars":var d=e.trim(c.text());a.remaining.chars=d.length-a.o.truncate;d.length>a.o.truncate&&(c.data("tHTML",c.html()),d=a.pad(d.slice(0,
a.o.truncate),d.slice(a.o.truncate,d.length)),c.html(d).removeClass(a.o.showClass).addClass(a.o.hideClass),g());break;case "words":d=e.trim(c.text());d=d.split(" ");a.remaining.words=d.length-a.o.truncate;d.length>a.o.truncate&&(c.data("tHTML",c.html()),d=a.pad(d.slice(0,a.o.truncate).join(" "),d.slice(a.o.truncate,d.length).join(" ")),c.html(d).removeClass(a.o.showClass).addClass(a.o.hideClass),g());break;case "lines":0==c.children("div").length&&c.wrapInner("<div>");d=c.children("div").css("height",
"");d.html(d.text());var h=d.height();"undefined"===typeof c.data("lHeight")?(temp=d.clone(),lHeight=temp.text("a").insertAfter(d).height(),c.data("lHeight",lHeight),d.next().remove()):lHeight=c.data("lHeight");lines=h/lHeight;a.remaining.lines=lines-a.o.truncate;0<a.remaining.lines&&(d.css("overflow","hidden"),d.animate({height:lHeight*a.o.truncate},f).data("tHeight",h),c.removeClass(a.o.showClass).addClass(a.o.hideClass),0!=c.find("[data-ctrl]").length||e.isFunction(a.o.controlBtn)||c.append(a.ctrlHtml),
g());break;case "block":a.blockMode(c,"hide",f,g)}a.status=0;"block"==a.mode?c.unbind("click.coll").bind("click.coll",function(b){b.preventDefault();a.show(c)}):(a.ctrlBtn=e.isFunction(a.o.controlBtn)?a.o.controlBtn.call(a.e):e(c.find("[data-ctrl]")),a.ctrlBtn.off("click.coll").on("click.coll",function(b){b.preventDefault();a.show(c)}).html(a.o.showText),g=a.o.showText,d={chars:["character","characters"],words:["word","words"],lines:["lines","lines"]},g=g.replace("%s",a.remaining[a.mode]+(1==a.remaining[a.mode]?
" "+d[a.mode][0]:" "+d[a.mode][1])),a.ctrlBtn.html(g))},pad:function(b,f){return b+'<span class="coll-ellipsis">'+this.o.ellipsis+"</span>"+(e.isFunction(this.o.ctrlBtn)?"":this.ctrlHtml)+'<span class="coll-hidden" style="display:none">'+f+"</span>"},blockMode:function(b,f,a,c){var g=["fadeOut","slideUp","fadeIn","slideDown"],d="fade"==this.o.effect?0:1,g="hide"==f?g[d]:g[d+2];if(e.isFunction(this.o.target))this.o.target.call(this.e)[g](a,c);else if(e.fn[this.o.target])e(b)[this.o.target]()[g](a,
c);"show"==f?(b.removeClass(this.o.showClass).addClass(this.o.hideClass),this.o.changeText&&b.text(this.o.hideText)):(b.removeClass(this.o.hideClass).addClass(this.o.showClass),this.o.changeText&&b.text(this.o.showText))},reInit:function(b){b.find("[data-ctrl]").remove();b.html(this.e.data("oCnt"));0==this.status?this.hide(b,0):this.show(b,0)}};e.fn.collapser=function(b){return this.each(function(){e.data(this,"collapser")||e.data(this,"collapser",new l(this,b))})}})(jQuery,window,document);

View file

@ -1,73 +1,93 @@
$(document).ready(function() {
function make_row(indexer_id, season, episode, name, checked) {
var checkedbox = (checked ? ' checked' : ''),
row_class = $('#row_class').val();
function make_row(indexer_id, season, episode, name, checked, airdate_never) {
var checkedbox = (checked ? ' checked' : ''),
row_class = $('#row_class').val(),
ep_id = season + 'x' + episode;
return ' <tr class="' + row_class + '">'
+ ' <td class="tableleft" align="center">'
+ '<input type="checkbox"'
+ ' class="' + indexer_id + '-epcheck"'
+ ' name="' + indexer_id + '-' + season + 'x' + episode + '"'
+ checkedbox+'></td>'
+ ' <td>' + season + 'x' + episode + '</td>'
+ ' <td class="tableright" style="width: 100%">' + name + '</td>'
+ ' </tr>';
}
return ' <tr id="ep-' + indexer_id + '-' + ep_id + '" class="' + (airdate_never ? 'airdate-never' : row_class) + '">'
+ ' <td class="tableleft" align="center">'
+ '<input type="checkbox"'
+ ' class="' + indexer_id + '-epcheck"'
+ ' name="' + indexer_id + '-' + ep_id + '"'
+ checkedbox+'></td>'
+ ' <td>' + ep_id + '</td>'
+ ' <td class="tableright" style="width: 100%">' + name + (airdate_never ? ' (<strong><em>airdate is never, this should change in time</em></strong>)' : '') + '</td>'
+ ' </tr>';
}
$('.go').click(function() {
var selected;
$('.go').click(function() {
if ($('input[class*="-epcheck"]:checked').length === 0 && $('input[id*="allCheck-"]:checked').length === 0) {
alert('Please select at least one Show or Episode');
return false
}
});
if (selected = (0 === $('input[class*="-epcheck"]:checked').length))
alert('Please select at least one episode');
$('.allCheck').click(function(){
var indexer_id = $(this).attr('id').split('-')[1];
$('.' + indexer_id + '-epcheck').prop('checked', $(this).prop('checked'));
});
return !selected
});
$('.get_more_eps').show();
function show_episodes(btn_element) {
var match = btn_element.attr('id').match(/(.*)[-](.*)/);
if (null == match)
return false;
$('.allCheck').click(function(){
var indexer_id = $(this).attr('id').split('-')[1];
$('.' + indexer_id + '-epcheck').prop('checked', $(this).prop('checked'));
});
var cur_indexer_id = match[1], action = match[2], checked = $('#allCheck-' + cur_indexer_id).prop('checked'),
show_header = $('tr#' + cur_indexer_id), episode_rows = $('tr[id*="ep-' + cur_indexer_id + '"]'),
void_var = 'more' == action && episode_rows.show() || episode_rows.hide();
$('.get_more_eps').click(function(){
var cur_indexer_id = $(this).attr('id');
var checked = $('#allCheck-' + cur_indexer_id).prop('checked');
var last_row = $('tr#' + cur_indexer_id);
$.getJSON(sbRoot + '/manage/showEpisodeStatuses',
{
indexer_id: cur_indexer_id,
whichStatus: $('#oldStatus').val()
},
function (data) {
$.each(data, function(season,eps){
$.each(eps, function(episode, name) {
//alert(season+'x'+episode+': '+name);
last_row.after(make_row(cur_indexer_id, season, episode, name, checked));
});
});
});
$(this).hide();
});
$('input#' + match[0]).val('more' == action ? 'Expanding...' : 'Collapsing...');
// selects all visible episode checkboxes.
$('.selectAllShows').click(function(){
$('.allCheck').each(function(){
this.checked = true;
});
$('input[class*="-epcheck"]').each(function(){
this.checked = true;
});
});
if (0 == episode_rows.length) {
$.getJSON(sbRoot + '/manage/showEpisodeStatuses',
{
indexer_id: cur_indexer_id,
whichStatus: $('#oldStatus').val()
},
function (data) {
$.each(data, function(season, eps){
$.each(eps, function(episode, meta) {
show_header.after(make_row(cur_indexer_id, season, episode, meta.name, checked, meta.airdate_never));
});
});
$('input#' + match[0]).val('more' == action ? 'Expand' : 'Collapse');
btn_element.hide();
$('input[id="' + cur_indexer_id + '-' + ('more' == action ? 'less' : 'more') + '"]').show();
});
} else {
$('input#' + match[0]).val('more' == action ? 'Expand' : 'Collapse');
btn_element.hide();
$('input[id="' + cur_indexer_id + '-' + ('more' == action ? 'less' : 'more') + '"]').show();
}
// clears all visible episode checkboxes and the season selectors
$('.unselectAllShows').click(function(){
$('.allCheck').each(function(){
this.checked = false;
});
$('input[class*="-epcheck"]').each(function(){
this.checked = false;
});
});
}
$('.get_more_eps,.get_less_eps').click(function(){
show_episodes($(this));
($('.get_more_eps:visible').length == 0 ? $('.expandAll').hide() : '');
});
$('.expandAll').click(function() {
$(this).hide();
$('.get_more_eps').each(function() {
show_episodes($(this));
});
});
// selects all visible episode checkboxes.
$('.selectAllShows').click(function(){
$('.sickbeardTable input').each(function() {
this.checked = true;
});
});
// clears all visible episode checkboxes and the season selectors
$('.unselectAllShows').click(function(){
$('.sickbeardTable input').each(function() {
this.checked = false;
});
});
});

View file

@ -1,78 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
"""
StringMatcher.py
ported from python-Levenshtein
[https://github.com/miohtama/python-Levenshtein]
"""
from Levenshtein import *
from warnings import warn
class StringMatcher:
"""A SequenceMatcher-like class built on the top of Levenshtein"""
def _reset_cache(self):
self._ratio = self._distance = None
self._opcodes = self._editops = self._matching_blocks = None
def __init__(self, isjunk=None, seq1='', seq2=''):
if isjunk:
warn("isjunk not NOT implemented, it will be ignored")
self._str1, self._str2 = seq1, seq2
self._reset_cache()
def set_seqs(self, seq1, seq2):
self._str1, self._str2 = seq1, seq2
self._reset_cache()
def set_seq1(self, seq1):
self._str1 = seq1
self._reset_cache()
def set_seq2(self, seq2):
self._str2 = seq2
self._reset_cache()
def get_opcodes(self):
if not self._opcodes:
if self._editops:
self._opcodes = opcodes(self._editops, self._str1, self._str2)
else:
self._opcodes = opcodes(self._str1, self._str2)
return self._opcodes
def get_editops(self):
if not self._editops:
if self._opcodes:
self._editops = editops(self._opcodes, self._str1, self._str2)
else:
self._editops = editops(self._str1, self._str2)
return self._editops
def get_matching_blocks(self):
if not self._matching_blocks:
self._matching_blocks = matching_blocks(self.get_opcodes(),
self._str1, self._str2)
return self._matching_blocks
def ratio(self):
if not self._ratio:
self._ratio = ratio(self._str1, self._str2)
return self._ratio
def quick_ratio(self):
# This is usually quick enough :o)
if not self._ratio:
self._ratio = ratio(self._str1, self._str2)
return self._ratio
def real_quick_ratio(self):
len1, len2 = len(self._str1), len(self._str2)
return 2.0 * min(len1, len2) / (len1 + len2)
def distance(self):
if not self._distance:
self._distance = distance(self._str1, self._str2)
return self._distance

View file

@ -1,263 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
"""
fuzz.py
Copyright (c) 2011 Adam Cohen
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from __future__ import unicode_literals
try:
from StringMatcher import StringMatcher as SequenceMatcher
except:
from difflib import SequenceMatcher
from . import utils
###########################
# Basic Scoring Functions #
###########################
def ratio(s1, s2):
if s1 is None:
raise TypeError("s1 is None")
if s2 is None:
raise TypeError("s2 is None")
s1, s2 = utils.make_type_consistent(s1, s2)
if len(s1) == 0 or len(s2) == 0:
return 0
m = SequenceMatcher(None, s1, s2)
return utils.intr(100 * m.ratio())
# todo: skip duplicate indexes for a little more speed
def partial_ratio(s1, s2):
if s1 is None:
raise TypeError("s1 is None")
if s2 is None:
raise TypeError("s2 is None")
s1, s2 = utils.make_type_consistent(s1, s2)
if len(s1) == 0 or len(s2) == 0:
return 0
if len(s1) <= len(s2):
shorter = s1
longer = s2
else:
shorter = s2
longer = s1
m = SequenceMatcher(None, shorter, longer)
blocks = m.get_matching_blocks()
# each block represents a sequence of matching characters in a string
# of the form (idx_1, idx_2, len)
# the best partial match will block align with at least one of those blocks
# e.g. shorter = "abcd", longer = XXXbcdeEEE
# block = (1,3,3)
# best score === ratio("abcd", "Xbcd")
scores = []
for block in blocks:
long_start = block[1] - block[0] if (block[1] - block[0]) > 0 else 0
long_end = long_start + len(shorter)
long_substr = longer[long_start:long_end]
m2 = SequenceMatcher(None, shorter, long_substr)
r = m2.ratio()
if r > .995:
return 100
else:
scores.append(r)
return int(100 * max(scores))
##############################
# Advanced Scoring Functions #
##############################
# Sorted Token
# find all alphanumeric tokens in the string
# sort those tokens and take ratio of resulting joined strings
# controls for unordered string elements
def _token_sort(s1, s2, partial=True, force_ascii=True):
if s1 is None:
raise TypeError("s1 is None")
if s2 is None:
raise TypeError("s2 is None")
# pull tokens
tokens1 = utils.full_process(s1, force_ascii=force_ascii).split()
tokens2 = utils.full_process(s2, force_ascii=force_ascii).split()
# sort tokens and join
sorted1 = " ".join(sorted(tokens1))
sorted2 = " ".join(sorted(tokens2))
sorted1 = sorted1.strip()
sorted2 = sorted2.strip()
if partial:
return partial_ratio(sorted1, sorted2)
else:
return ratio(sorted1, sorted2)
def token_sort_ratio(s1, s2, force_ascii=True):
return _token_sort(s1, s2, partial=False, force_ascii=force_ascii)
def partial_token_sort_ratio(s1, s2, force_ascii=True):
return _token_sort(s1, s2, partial=True, force_ascii=force_ascii)
# Token Set
# find all alphanumeric tokens in each string...treat them as a set
# construct two strings of the form
# <sorted_intersection><sorted_remainder>
# take ratios of those two strings
# controls for unordered partial matches
def _token_set(s1, s2, partial=True, force_ascii=True):
if s1 is None:
raise TypeError("s1 is None")
if s2 is None:
raise TypeError("s2 is None")
p1 = utils.full_process(s1, force_ascii=force_ascii)
p2 = utils.full_process(s2, force_ascii=force_ascii)
if not utils.validate_string(p1):
return 0
if not utils.validate_string(p2):
return 0
# pull tokens
tokens1 = set(utils.full_process(p1).split())
tokens2 = set(utils.full_process(p2).split())
intersection = tokens1.intersection(tokens2)
diff1to2 = tokens1.difference(tokens2)
diff2to1 = tokens2.difference(tokens1)
sorted_sect = " ".join(sorted(intersection))
sorted_1to2 = " ".join(sorted(diff1to2))
sorted_2to1 = " ".join(sorted(diff2to1))
combined_1to2 = sorted_sect + " " + sorted_1to2
combined_2to1 = sorted_sect + " " + sorted_2to1
# strip
sorted_sect = sorted_sect.strip()
combined_1to2 = combined_1to2.strip()
combined_2to1 = combined_2to1.strip()
pairwise = [
ratio(sorted_sect, combined_1to2),
ratio(sorted_sect, combined_2to1),
ratio(combined_1to2, combined_2to1)
]
return max(pairwise)
def token_set_ratio(s1, s2, force_ascii=True):
return _token_set(s1, s2, partial=False, force_ascii=force_ascii)
def partial_token_set_ratio(s1, s2, force_ascii=True):
return _token_set(s1, s2, partial=True, force_ascii=force_ascii)
# TODO: numerics
###################
# Combination API #
###################
# q is for quick
def QRatio(s1, s2, force_ascii=True):
p1 = utils.full_process(s1, force_ascii=force_ascii)
p2 = utils.full_process(s2, force_ascii=force_ascii)
if not utils.validate_string(p1):
return 0
if not utils.validate_string(p2):
return 0
return ratio(p1, p2)
def UQRatio(s1, s2):
return QRatio(s1, s2, force_ascii=False)
# w is for weighted
def WRatio(s1, s2, force_ascii=True):
p1 = utils.full_process(s1, force_ascii=force_ascii)
p2 = utils.full_process(s2, force_ascii=force_ascii)
if not utils.validate_string(p1):
return 0
if not utils.validate_string(p2):
return 0
# should we look at partials?
try_partial = True
unbase_scale = .95
partial_scale = .90
base = ratio(p1, p2)
len_ratio = float(max(len(p1), len(p2))) / min(len(p1), len(p2))
# if strings are similar length, don't use partials
if len_ratio < 1.5:
try_partial = False
# if one string is much much shorter than the other
if len_ratio > 8:
partial_scale = .6
if try_partial:
partial = partial_ratio(p1, p2) * partial_scale
ptsor = partial_token_sort_ratio(p1, p2, force_ascii=force_ascii) \
* unbase_scale * partial_scale
ptser = partial_token_set_ratio(p1, p2, force_ascii=force_ascii) \
* unbase_scale * partial_scale
return int(max(base, partial, ptsor, ptser))
else:
tsor = token_sort_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale
tser = token_set_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale
return int(max(base, tsor, tser))
def UWRatio(s1, s2):
return WRatio(s1, s2, force_ascii=False)

View file

@ -1,119 +0,0 @@
#!/usr/bin/env python
# encoding: utf-8
"""
process.py
Copyright (c) 2011 Adam Cohen
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import itertools
from . import fuzz
from . import utils
def extract(query, choices, processor=None, scorer=None, limit=5):
"""Find best matches in a list of choices, return a list of tuples
containing the match and it's score.
Arguments:
query -- an object representing the thing we want to find
choices -- a list of objects we are attempting to extract
values from
scorer -- f(OBJ, QUERY) --> INT. We will return the objects
with the highest score by default, we use
score.WRatio() and both OBJ and QUERY should be
strings
processor -- f(OBJ_A) --> OBJ_B, where the output is an input
to scorer for example, "processor = lambda x:
x[0]" would return the first element in a
collection x (of, say, strings) this would then
be used in the scoring collection by default, we
use utils.full_process()
"""
if choices is None or len(choices) == 0:
return []
# default, turn whatever the choice is into a workable string
if processor is None:
processor = lambda x: utils.full_process(x)
# default: wratio
if scorer is None:
scorer = fuzz.WRatio
sl = list()
for choice in choices:
processed = processor(choice)
score = scorer(query, processed)
tuple = (choice, score)
sl.append(tuple)
sl.sort(key=lambda i: i[1], reverse=True)
return sl[:limit]
def extractBests(query, choices, processor=None, scorer=None, score_cutoff=0, limit=5):
"""Find best matches above a score in a list of choices, return a
list of tuples containing the match and it's score.
Convenience method which returns the choices with best scores, see
extract() for full arguments list
Optional parameter: score_cutoff.
If the choice has a score of less than or equal to score_cutoff
it will not be included on result list
"""
best_list = extract(query, choices, processor, scorer, limit)
if len(best_list) > 0:
return list(itertools.takewhile(lambda x: x[1] > score_cutoff, best_list))
else:
return []
def extractOne(query, choices, processor=None, scorer=None, score_cutoff=0):
"""Find the best match above a score in a list of choices, return a
tuple containing the match and it's score if it's above the treshold
or None.
Convenience method which returns the single best choice, see
extract() for full arguments list
Optional parameter: score_cutoff.
If the best choice has a score of less than or equal to
score_cutoff we will return none (intuition: not a good enough
match)
"""
best_list = extract(query, choices, processor, scorer, limit=1)
if len(best_list) > 0:
best = best_list[0]
if best[1] > score_cutoff:
return best
else:
return None
else:
return None

View file

@ -1,41 +0,0 @@
from __future__ import unicode_literals
import re
class StringProcessor(object):
"""
This class defines method to process strings in the most
efficient way. Ideally all the methods below use unicode strings
for both input and output.
"""
@classmethod
def replace_non_letters_non_numbers_with_whitespace(cls, a_string):
"""
This function replaces any sequence of non letters and non
numbers with a single white space.
"""
regex = re.compile(r"(?ui)\W")
return regex.sub(" ", a_string)
@classmethod
def strip(cls, a_string):
"""
This function strips leading and trailing white space.
"""
return a_string.strip()
@classmethod
def to_lower_case(cls, a_string):
"""
This function returns the lower-cased version of the string given.
"""
return a_string.lower()
@classmethod
def to_upper_case(cls, a_string):
"""
This function returns the upper-cased version of the string given.
"""
return a_string.upper()

View file

@ -1,76 +0,0 @@
from __future__ import unicode_literals
import sys
from fuzzywuzzy.string_processing import StringProcessor
PY3 = sys.version_info[0] == 3
def validate_string(s):
try:
if len(s) > 0:
return True
else:
return False
except:
return False
bad_chars = str('') # ascii dammit!
for i in range(128, 256):
bad_chars += chr(i)
if PY3:
translation_table = dict((ord(c), None) for c in bad_chars)
def asciionly(s):
if PY3:
return s.translate(translation_table)
else:
return s.translate(None, bad_chars)
def asciidammit(s):
if type(s) is str:
return asciionly(s)
elif type(s) is unicode:
return asciionly(s.encode('ascii', 'ignore'))
else:
return asciidammit(unicode(s))
def make_type_consistent(s1, s2):
if isinstance(s1, str) and isinstance(s2, str):
return s1, s2
elif isinstance(s1, unicode) and isinstance(s2, unicode):
return s1, s2
else:
return unicode(s1), unicode(s2)
def full_process(s, force_ascii=False):
"""Process string by
-- removing all but letters and numbers
-- trim whitespace
-- force to lower case
if force_ascii == True, force convert to ascii"""
if s is None:
return ""
if force_ascii:
s = asciidammit(s)
# Keep only Letters and Numbres (see Unicode docs).
string_out = StringProcessor.replace_non_letters_non_numbers_with_whitespace(s)
# Force into lowercase.
string_out = StringProcessor.to_lower_case(string_out)
# Remove leading and trailing whitespaces.
string_out = StringProcessor.strip(string_out)
return string_out
def intr(n):
'''Returns a correctly rounded integer'''
return int(round(n))

View file

@ -1,18 +0,0 @@
from pysrt.srttime import SubRipTime
from pysrt.srtitem import SubRipItem
from pysrt.srtfile import SubRipFile
from pysrt.srtexc import Error, InvalidItem, InvalidTimeString
from pysrt.version import VERSION, VERSION_STRING
__all__ = [
'SubRipFile', 'SubRipItem', 'SubRipFile', 'SUPPORT_UTF_32_LE',
'SUPPORT_UTF_32_BE', 'InvalidItem', 'InvalidTimeString'
]
ERROR_PASS = SubRipFile.ERROR_PASS
ERROR_LOG = SubRipFile.ERROR_LOG
ERROR_RAISE = SubRipFile.ERROR_RAISE
open = SubRipFile.open
stream = SubRipFile.stream
from_string = SubRipFile.from_string

View file

@ -1,218 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable-all
import os
import re
import sys
import codecs
import shutil
import argparse
from textwrap import dedent
from chardet import detect
from pysrt import SubRipFile, SubRipTime, VERSION_STRING
def underline(string):
return "\033[4m%s\033[0m" % string
class TimeAwareArgumentParser(argparse.ArgumentParser):
RE_TIME_REPRESENTATION = re.compile(r'^\-?(\d+[hms]{0,2}){1,4}$')
def parse_args(self, args=None, namespace=None):
time_index = -1
for index, arg in enumerate(args):
match = self.RE_TIME_REPRESENTATION.match(arg)
if match:
time_index = index
break
if time_index >= 0:
args.insert(time_index, '--')
return super(TimeAwareArgumentParser, self).parse_args(args, namespace)
class SubRipShifter(object):
BACKUP_EXTENSION = '.bak'
RE_TIME_STRING = re.compile(r'(\d+)([hms]{0,2})')
UNIT_RATIOS = {
'ms': 1,
'': SubRipTime.SECONDS_RATIO,
's': SubRipTime.SECONDS_RATIO,
'm': SubRipTime.MINUTES_RATIO,
'h': SubRipTime.HOURS_RATIO,
}
DESCRIPTION = dedent("""\
Srt subtitle editor
It can either shift, split or change the frame rate.
""")
TIMESTAMP_HELP = "A timestamp in the form: [-][Hh][Mm]S[s][MSms]"
SHIFT_EPILOG = dedent("""\
Examples:
1 minute and 12 seconds foreward (in place):
$ srt -i shift 1m12s movie.srt
half a second foreward:
$ srt shift 500ms movie.srt > othername.srt
1 second and half backward:
$ srt -i shift -1s500ms movie.srt
3 seconds backward:
$ srt -i shift -3 movie.srt
""")
RATE_EPILOG = dedent("""\
Examples:
Convert 23.9fps subtitles to 25fps:
$ srt -i rate 23.9 25 movie.srt
""")
LIMITS_HELP = "Each parts duration in the form: [Hh][Mm]S[s][MSms]"
SPLIT_EPILOG = dedent("""\
Examples:
For a movie in 2 parts with the first part 48 minutes and 18 seconds long:
$ srt split 48m18s movie.srt
=> creates movie.1.srt and movie.2.srt
For a movie in 3 parts of 20 minutes each:
$ srt split 20m 20m movie.srt
=> creates movie.1.srt, movie.2.srt and movie.3.srt
""")
FRAME_RATE_HELP = "A frame rate in fps (commonly 23.9 or 25)"
ENCODING_HELP = dedent("""\
Change file encoding. Useful for players accepting only latin1 subtitles.
List of supported encodings: http://docs.python.org/library/codecs.html#standard-encodings
""")
BREAK_EPILOG = dedent("""\
Break lines longer than defined length
""")
LENGTH_HELP = "Maximum number of characters per line"
def __init__(self):
self.output_file_path = None
def build_parser(self):
parser = TimeAwareArgumentParser(description=self.DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('-i', '--in-place', action='store_true', dest='in_place',
help="Edit file in-place, saving a backup as file.bak (do not works for the split command)")
parser.add_argument('-e', '--output-encoding', metavar=underline('encoding'), action='store', dest='output_encoding',
type=self.parse_encoding, help=self.ENCODING_HELP)
parser.add_argument('-v', '--version', action='version', version='%%(prog)s %s' % VERSION_STRING)
subparsers = parser.add_subparsers(title='commands')
shift_parser = subparsers.add_parser('shift', help="Shift subtitles by specified time offset", epilog=self.SHIFT_EPILOG, formatter_class=argparse.RawTextHelpFormatter)
shift_parser.add_argument('time_offset', action='store', metavar=underline('offset'),
type=self.parse_time, help=self.TIMESTAMP_HELP)
shift_parser.set_defaults(action=self.shift)
rate_parser = subparsers.add_parser('rate', help="Convert subtitles from a frame rate to another", epilog=self.RATE_EPILOG, formatter_class=argparse.RawTextHelpFormatter)
rate_parser.add_argument('initial', action='store', type=float, help=self.FRAME_RATE_HELP)
rate_parser.add_argument('final', action='store', type=float, help=self.FRAME_RATE_HELP)
rate_parser.set_defaults(action=self.rate)
split_parser = subparsers.add_parser('split', help="Split a file in multiple parts", epilog=self.SPLIT_EPILOG, formatter_class=argparse.RawTextHelpFormatter)
split_parser.add_argument('limits', action='store', nargs='+', type=self.parse_time, help=self.LIMITS_HELP)
split_parser.set_defaults(action=self.split)
break_parser = subparsers.add_parser('break', help="Break long lines", epilog=self.BREAK_EPILOG, formatter_class=argparse.RawTextHelpFormatter)
break_parser.add_argument('length', action='store', type=int, help=self.LENGTH_HELP)
break_parser.set_defaults(action=self.break_lines)
parser.add_argument('file', action='store')
return parser
def run(self, args):
self.arguments = self.build_parser().parse_args(args)
if self.arguments.in_place:
self.create_backup()
self.arguments.action()
def parse_time(self, time_string):
negative = time_string.startswith('-')
if negative:
time_string = time_string[1:]
ordinal = sum(int(value) * self.UNIT_RATIOS[unit] for value, unit
in self.RE_TIME_STRING.findall(time_string))
return -ordinal if negative else ordinal
def parse_encoding(self, encoding_name):
try:
codecs.lookup(encoding_name)
except LookupError as error:
raise argparse.ArgumentTypeError(error.message)
return encoding_name
def shift(self):
self.input_file.shift(milliseconds=self.arguments.time_offset)
self.input_file.write_into(self.output_file)
def rate(self):
ratio = self.arguments.final / self.arguments.initial
self.input_file.shift(ratio=ratio)
self.input_file.write_into(self.output_file)
def split(self):
limits = [0] + self.arguments.limits + [self.input_file[-1].end.ordinal + 1]
base_name, extension = os.path.splitext(self.arguments.file)
for index, (start, end) in enumerate(zip(limits[:-1], limits[1:])):
file_name = '%s.%s%s' % (base_name, index + 1, extension)
part_file = self.input_file.slice(ends_after=start, starts_before=end)
part_file.shift(milliseconds=-start)
part_file.clean_indexes()
part_file.save(path=file_name, encoding=self.output_encoding)
def create_backup(self):
backup_file = self.arguments.file + self.BACKUP_EXTENSION
if not os.path.exists(backup_file):
shutil.copy2(self.arguments.file, backup_file)
self.output_file_path = self.arguments.file
self.arguments.file = backup_file
def break_lines(self):
split_re = re.compile(r'(.{,%i})(?:\s+|$)' % self.arguments.length)
for item in self.input_file:
item.text = '\n'.join(split_re.split(item.text)[1::2])
self.input_file.write_into(self.output_file)
@property
def output_encoding(self):
return self.arguments.output_encoding or self.input_file.encoding
@property
def input_file(self):
if not hasattr(self, '_source_file'):
with open(self.arguments.file, 'rb') as f:
content = f.read()
encoding = detect(content).get('encoding')
encoding = self.normalize_encoding(encoding)
self._source_file = SubRipFile.open(self.arguments.file,
encoding=encoding, error_handling=SubRipFile.ERROR_LOG)
return self._source_file
@property
def output_file(self):
if not hasattr(self, '_output_file'):
if self.output_file_path:
self._output_file = codecs.open(self.output_file_path, 'w+', encoding=self.output_encoding)
else:
self._output_file = sys.stdout
return self._output_file
def normalize_encoding(self, encoding):
return encoding.lower().replace('-', '_')
def main():
SubRipShifter().run(sys.argv[1:])
if __name__ == '__main__':
main()

View file

@ -1,26 +0,0 @@
class ComparableMixin(object):
def _compare(self, other, method):
try:
return method(self._cmpkey(), other._cmpkey())
except (AttributeError, TypeError):
# _cmpkey not implemented, or return different type,
# so I can't compare with "other".
return NotImplemented
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)

View file

@ -1,24 +0,0 @@
import sys
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
from io import open as io_open
if is_py2:
builtin_str = str
basestring = basestring
str = unicode
open = io_open
elif is_py3:
builtin_str = str
basestring = (str, bytes)
str = str
open = open

View file

@ -1,31 +0,0 @@
"""
Exception classes
"""
class Error(Exception):
"""
Pysrt's base exception
"""
pass
class InvalidTimeString(Error):
"""
Raised when parser fail on bad formated time strings
"""
pass
class InvalidItem(Error):
"""
Raised when parser fail to parse a sub title item
"""
pass
class InvalidIndex(InvalidItem):
"""
Raised when parser fail to parse a sub title index
"""
pass

View file

@ -1,312 +0,0 @@
# -*- coding: utf-8 -*-
import os
import sys
import codecs
try:
from collections import UserList
except ImportError:
from UserList import UserList
from itertools import chain
from copy import copy
from pysrt.srtexc import Error
from pysrt.srtitem import SubRipItem
from pysrt.compat import str
BOMS = ((codecs.BOM_UTF32_LE, 'utf_32_le'),
(codecs.BOM_UTF32_BE, 'utf_32_be'),
(codecs.BOM_UTF16_LE, 'utf_16_le'),
(codecs.BOM_UTF16_BE, 'utf_16_be'),
(codecs.BOM_UTF8, 'utf_8'))
CODECS_BOMS = dict((codec, str(bom, codec)) for bom, codec in BOMS)
BIGGER_BOM = max(len(bom) for bom, encoding in BOMS)
class SubRipFile(UserList, object):
"""
SubRip file descriptor.
Provide a pure Python mapping on all metadata.
SubRipFile(items, eol, path, encoding)
items -> list of SubRipItem. Default to [].
eol -> str: end of line character. Default to linesep used in opened file
if any else to os.linesep.
path -> str: path where file will be saved. To open an existant file see
SubRipFile.open.
encoding -> str: encoding used at file save. Default to utf-8.
"""
ERROR_PASS = 0
ERROR_LOG = 1
ERROR_RAISE = 2
DEFAULT_ENCODING = 'utf_8'
def __init__(self, items=None, eol=None, path=None, encoding='utf-8'):
UserList.__init__(self, items or [])
self._eol = eol
self.path = path
self.encoding = encoding
def _get_eol(self):
return self._eol or os.linesep
def _set_eol(self, eol):
self._eol = self._eol or eol
eol = property(_get_eol, _set_eol)
def slice(self, starts_before=None, starts_after=None, ends_before=None,
ends_after=None):
"""
slice([starts_before][, starts_after][, ends_before][, ends_after]) \
-> SubRipFile clone
All arguments are optional, and should be coercible to SubRipTime
object.
It reduce the set of subtitles to those that match match given time
constraints.
The returned set is a clone, but still contains references to original
subtitles. So if you shift this returned set, subs contained in the
original SubRipFile instance will be altered too.
Example:
>>> subs.slice(ends_after={'seconds': 20}).shift(seconds=2)
"""
clone = copy(self)
if starts_before:
clone.data = (i for i in clone.data if i.start < starts_before)
if starts_after:
clone.data = (i for i in clone.data if i.start > starts_after)
if ends_before:
clone.data = (i for i in clone.data if i.end < ends_before)
if ends_after:
clone.data = (i for i in clone.data if i.end > ends_after)
clone.data = list(clone.data)
return clone
def at(self, timestamp=None, **kwargs):
"""
at(timestamp) -> SubRipFile clone
timestamp argument should be coercible to SubRipFile object.
A specialization of slice. Return all subtiles visible at the
timestamp mark.
Example:
>>> subs.at((0, 0, 20, 0)).shift(seconds=2)
>>> subs.at(seconds=20).shift(seconds=2)
"""
time = timestamp or kwargs
return self.slice(starts_before=time, ends_after=time)
def shift(self, *args, **kwargs):
"""shift(hours, minutes, seconds, milliseconds, ratio)
Shift `start` and `end` attributes of each items of file either by
applying a ratio or by adding an offset.
`ratio` should be either an int or a float.
Example to convert subtitles from 23.9 fps to 25 fps:
>>> subs.shift(ratio=25/23.9)
All "time" arguments are optional and have a default value of 0.
Example to delay all subs from 2 seconds and half
>>> subs.shift(seconds=2, milliseconds=500)
"""
for item in self:
item.shift(*args, **kwargs)
def clean_indexes(self):
"""
clean_indexes()
Sort subs and reset their index attribute. Should be called after
destructive operations like split or such.
"""
self.sort()
for index, item in enumerate(self):
item.index = index + 1
@property
def text(self):
return '\n'.join(i.text for i in self)
@classmethod
def open(cls, path='', encoding=None, error_handling=ERROR_PASS):
"""
open([path, [encoding]])
If you do not provide any encoding, it can be detected if the file
contain a bit order mark, unless it is set to utf-8 as default.
"""
new_file = cls(path=path, encoding=encoding)
source_file = cls._open_unicode_file(path, claimed_encoding=encoding)
new_file.read(source_file, error_handling=error_handling)
source_file.close()
return new_file
@classmethod
def from_string(cls, source, **kwargs):
"""
from_string(source, **kwargs) -> SubRipFile
`source` -> a unicode instance or at least a str instance encoded with
`sys.getdefaultencoding()`
"""
error_handling = kwargs.pop('error_handling', None)
new_file = cls(**kwargs)
new_file.read(source.splitlines(True), error_handling=error_handling)
return new_file
def read(self, source_file, error_handling=ERROR_PASS):
"""
read(source_file, [error_handling])
This method parse subtitles contained in `source_file` and append them
to the current instance.
`source_file` -> Any iterable that yield unicode strings, like a file
opened with `codecs.open()` or an array of unicode.
"""
self.eol = self._guess_eol(source_file)
self.extend(self.stream(source_file, error_handling=error_handling))
return self
@classmethod
def stream(cls, source_file, error_handling=ERROR_PASS):
"""
stream(source_file, [error_handling])
This method yield SubRipItem instances a soon as they have been parsed
without storing them. It is a kind of SAX parser for .srt files.
`source_file` -> Any iterable that yield unicode strings, like a file
opened with `codecs.open()` or an array of unicode.
Example:
>>> import pysrt
>>> import codecs
>>> file = codecs.open('movie.srt', encoding='utf-8')
>>> for sub in pysrt.stream(file):
... sub.text += "\nHello !"
... print unicode(sub)
"""
string_buffer = []
for index, line in enumerate(chain(source_file, '\n')):
if line.strip():
string_buffer.append(line)
else:
source = string_buffer
string_buffer = []
if source and all(source):
try:
yield SubRipItem.from_lines(source)
except Error as error:
error.args += (''.join(source), )
cls._handle_error(error, error_handling, index)
def save(self, path=None, encoding=None, eol=None):
"""
save([path][, encoding][, eol])
Use initial path if no other provided.
Use initial encoding if no other provided.
Use initial eol if no other provided.
"""
path = path or self.path
encoding = encoding or self.encoding
save_file = codecs.open(path, 'w+', encoding=encoding)
self.write_into(save_file, eol=eol)
save_file.close()
def write_into(self, output_file, eol=None):
"""
write_into(output_file [, eol])
Serialize current state into `output_file`.
`output_file` -> Any instance that respond to `write()`, typically a
file object
"""
output_eol = eol or self.eol
for item in self:
string_repr = str(item)
if output_eol != '\n':
string_repr = string_repr.replace('\n', output_eol)
output_file.write(string_repr)
# Only add trailing eol if it's not already present.
# It was kept in the SubRipItem's text before but it really
# belongs here. Existing applications might give us subtitles
# which already contain a trailing eol though.
if not string_repr.endswith(2 * output_eol):
output_file.write(output_eol)
@classmethod
def _guess_eol(cls, string_iterable):
first_line = cls._get_first_line(string_iterable)
for eol in ('\r\n', '\r', '\n'):
if first_line.endswith(eol):
return eol
return os.linesep
@classmethod
def _get_first_line(cls, string_iterable):
if hasattr(string_iterable, 'tell'):
previous_position = string_iterable.tell()
try:
first_line = next(iter(string_iterable))
except StopIteration:
return ''
if hasattr(string_iterable, 'seek'):
string_iterable.seek(previous_position)
return first_line
@classmethod
def _detect_encoding(cls, path):
file_descriptor = open(path, 'rb')
first_chars = file_descriptor.read(BIGGER_BOM)
file_descriptor.close()
for bom, encoding in BOMS:
if first_chars.startswith(bom):
return encoding
# TODO: maybe a chardet integration
return cls.DEFAULT_ENCODING
@classmethod
def _open_unicode_file(cls, path, claimed_encoding=None):
encoding = claimed_encoding or cls._detect_encoding(path)
source_file = codecs.open(path, 'rU', encoding=encoding)
# get rid of BOM if any
possible_bom = CODECS_BOMS.get(encoding, None)
if possible_bom:
file_bom = source_file.read(len(possible_bom))
if not file_bom == possible_bom:
source_file.seek(0) # if not rewind
return source_file
@classmethod
def _handle_error(cls, error, error_handling, index):
if error_handling == cls.ERROR_RAISE:
error.args = (index, ) + error.args
raise error
if error_handling == cls.ERROR_LOG:
name = type(error).__name__
sys.stderr.write('PySRT-%s(line %s): \n' % (name, index))
sys.stderr.write(error.args[0].encode('ascii', 'replace'))
sys.stderr.write('\n')

View file

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
"""
SubRip's subtitle parser
"""
from pysrt.srtexc import InvalidItem, InvalidIndex
from pysrt.srttime import SubRipTime
from pysrt.comparablemixin import ComparableMixin
from pysrt.compat import str
class SubRipItem(ComparableMixin):
"""
SubRipItem(index, start, end, text, position)
index -> int: index of item in file. 0 by default.
start, end -> SubRipTime or coercible.
text -> unicode: text content for item.
position -> unicode: raw srt/vtt "display coordinates" string
"""
ITEM_PATTERN = '%s\n%s --> %s%s\n%s\n'
TIMESTAMP_SEPARATOR = '-->'
def __init__(self, index=0, start=None, end=None, text='', position=''):
try:
self.index = int(index)
except (TypeError, ValueError): # try to cast as int, but it's not mandatory
self.index = index
self.start = SubRipTime.coerce(start or 0)
self.end = SubRipTime.coerce(end or 0)
self.position = str(position)
self.text = str(text)
def __str__(self):
position = ' %s' % self.position if self.position.strip() else ''
return self.ITEM_PATTERN % (self.index, self.start, self.end,
position, self.text)
def _cmpkey(self):
return (self.start, self.end)
def shift(self, *args, **kwargs):
"""
shift(hours, minutes, seconds, milliseconds, ratio)
Add given values to start and end attributes.
All arguments are optional and have a default value of 0.
"""
self.start.shift(*args, **kwargs)
self.end.shift(*args, **kwargs)
@classmethod
def from_string(cls, source):
return cls.from_lines(source.splitlines(True))
@classmethod
def from_lines(cls, lines):
if len(lines) < 2:
raise InvalidItem()
lines = [l.rstrip() for l in lines]
index = None
if cls.TIMESTAMP_SEPARATOR not in lines[0]:
index = lines.pop(0)
start, end, position = cls.split_timestamps(lines[0])
body = '\n'.join(lines[1:])
return cls(index, start, end, body, position)
@classmethod
def split_timestamps(cls, line):
timestamps = line.split(cls.TIMESTAMP_SEPARATOR)
if len(timestamps) != 2:
raise InvalidItem()
start, end_and_position = timestamps
end_and_position = end_and_position.lstrip().split(' ', 1)
end = end_and_position[0]
position = end_and_position[1] if len(end_and_position) > 1 else ''
return (s.strip() for s in (start, end, position))

View file

@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
"""
SubRip's time format parser: HH:MM:SS,mmm
"""
import re
from datetime import time
from pysrt.srtexc import InvalidTimeString
from pysrt.comparablemixin import ComparableMixin
from pysrt.compat import str, basestring
class TimeItemDescriptor(object):
# pylint: disable-msg=R0903
def __init__(self, ratio, super_ratio=0):
self.ratio = int(ratio)
self.super_ratio = int(super_ratio)
def _get_ordinal(self, instance):
if self.super_ratio:
return instance.ordinal % self.super_ratio
return instance.ordinal
def __get__(self, instance, klass):
if instance is None:
raise AttributeError
return self._get_ordinal(instance) // self.ratio
def __set__(self, instance, value):
part = self._get_ordinal(instance) - instance.ordinal % self.ratio
instance.ordinal += value * self.ratio - part
class SubRipTime(ComparableMixin):
TIME_PATTERN = '%02d:%02d:%02d,%03d'
TIME_REPR = 'SubRipTime(%d, %d, %d, %d)'
RE_TIME_SEP = re.compile(r'\:|\.|\,')
RE_INTEGER = re.compile(r'^(\d+)')
SECONDS_RATIO = 1000
MINUTES_RATIO = SECONDS_RATIO * 60
HOURS_RATIO = MINUTES_RATIO * 60
hours = TimeItemDescriptor(HOURS_RATIO)
minutes = TimeItemDescriptor(MINUTES_RATIO, HOURS_RATIO)
seconds = TimeItemDescriptor(SECONDS_RATIO, MINUTES_RATIO)
milliseconds = TimeItemDescriptor(1, SECONDS_RATIO)
def __init__(self, hours=0, minutes=0, seconds=0, milliseconds=0):
"""
SubRipTime(hours, minutes, seconds, milliseconds)
All arguments are optional and have a default value of 0.
"""
super(SubRipTime, self).__init__()
self.ordinal = hours * self.HOURS_RATIO \
+ minutes * self.MINUTES_RATIO \
+ seconds * self.SECONDS_RATIO \
+ milliseconds
def __repr__(self):
return self.TIME_REPR % tuple(self)
def __str__(self):
if self.ordinal < 0:
# Represent negative times as zero
return str(SubRipTime.from_ordinal(0))
return self.TIME_PATTERN % tuple(self)
def _compare(self, other, method):
return super(SubRipTime, self)._compare(self.coerce(other), method)
def _cmpkey(self):
return self.ordinal
def __add__(self, other):
return self.from_ordinal(self.ordinal + self.coerce(other).ordinal)
def __iadd__(self, other):
self.ordinal += self.coerce(other).ordinal
return self
def __sub__(self, other):
return self.from_ordinal(self.ordinal - self.coerce(other).ordinal)
def __isub__(self, other):
self.ordinal -= self.coerce(other).ordinal
return self
def __mul__(self, ratio):
return self.from_ordinal(int(round(self.ordinal * ratio)))
def __imul__(self, ratio):
self.ordinal = int(round(self.ordinal * ratio))
return self
@classmethod
def coerce(cls, other):
"""
Coerce many types to SubRipTime instance.
Supported types:
- str/unicode
- int/long
- datetime.time
- any iterable
- dict
"""
if isinstance(other, SubRipTime):
return other
if isinstance(other, basestring):
return cls.from_string(other)
if isinstance(other, int):
return cls.from_ordinal(other)
if isinstance(other, time):
return cls.from_time(other)
try:
return cls(**other)
except TypeError:
return cls(*other)
def __iter__(self):
yield self.hours
yield self.minutes
yield self.seconds
yield self.milliseconds
def shift(self, *args, **kwargs):
"""
shift(hours, minutes, seconds, milliseconds)
All arguments are optional and have a default value of 0.
"""
if 'ratio' in kwargs:
self *= kwargs.pop('ratio')
self += self.__class__(*args, **kwargs)
@classmethod
def from_ordinal(cls, ordinal):
"""
int -> SubRipTime corresponding to a total count of milliseconds
"""
return cls(milliseconds=int(ordinal))
@classmethod
def from_string(cls, source):
"""
str/unicode(HH:MM:SS,mmm) -> SubRipTime corresponding to serial
raise InvalidTimeString
"""
items = cls.RE_TIME_SEP.split(source)
if len(items) != 4:
raise InvalidTimeString
return cls(*(cls.parse_int(i) for i in items))
@classmethod
def parse_int(cls, digits):
try:
return int(digits)
except ValueError:
match = cls.RE_INTEGER.match(digits)
if match:
return int(match.group())
return 0
@classmethod
def from_time(cls, source):
"""
datetime.time -> SubRipTime corresponding to time object
"""
return cls(hours=source.hour, minutes=source.minute,
seconds=source.second, milliseconds=source.microsecond // 1000)
def to_time(self):
"""
Convert SubRipTime instance into a pure datetime.time object
"""
return time(self.hours, self.minutes, self.seconds,
self.milliseconds * 1000)

View file

@ -1,2 +0,0 @@
VERSION = (1, 0, 1)
VERSION_STRING = '.'.join(str(i) for i in VERSION)

View file

@ -938,7 +938,7 @@ class Tvdb:
# Item is integer, treat as show id
if key not in self.shows:
self._getShowData(key, self.config['language'], True)
return (None, self.shows[key])[key in self.shows]
return None if key not in self.shows else self.shows[key]
key = str(key).lower()
self.config['searchterm'] = key

View file

@ -660,7 +660,7 @@ class TVRage:
# Item is integer, treat as show id
if key not in self.shows:
self._getShowData(key, True)
return (None, self.shows[key])[key in self.shows]
return None if key not in self.shows else self.shows[key]
key = key.lower()
self.config['searchterm'] = key

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
Cheetah>=2.1.0

View file

@ -36,7 +36,7 @@ from sickbeard import providers, metadata, config, webserveInit
from sickbeard.providers.generic import GenericProvider
from providers import ezrss, btn, newznab, womble, thepiratebay, torrentleech, kat, iptorrents, \
omgwtfnzbs, scc, hdtorrents, torrentday, hdbits, nextgen, speedcd, nyaatorrents, torrentbytes, \
freshontv, bitsoup, tokyotoshokan
freshontv, bitsoup, tokyotoshokan, animenzb
from sickbeard.config import CheckSection, check_setting_int, check_setting_str, check_setting_float, ConfigMigrator, \
naming_ep_type, minimax
from sickbeard import searchBacklog, showUpdater, versionChecker, properFinder, autoPostProcesser, \
@ -60,7 +60,7 @@ CFG = None
CONFIG_FILE = None
# This is the version of the config we EXPECT to find
CONFIG_VERSION = 8
CONFIG_VERSION = 9
# Default encryption version (0 for None)
ENCRYPTION_VERSION = 0
@ -116,6 +116,7 @@ started = False
ACTUAL_LOG_DIR = None
LOG_DIR = None
FILE_LOGGING_PRESET = 'DB'
SOCKET_TIMEOUT = None
@ -153,6 +154,9 @@ TRASH_ROTATE_LOGS = False
HOME_SEARCH_FOCUS = True
SORT_ARTICLE = False
DEBUG = False
DISPLAY_BACKGROUND = False
DISPLAY_BACKGROUND_TRANSPARENT = None
DISPLAY_ALL_SEASONS = True
USE_LISTVIEW = False
METADATA_XBMC = None
@ -162,15 +166,19 @@ METADATA_PS3 = None
METADATA_WDTV = None
METADATA_TIVO = None
METADATA_MEDE8ER = None
METADATA_KODI = None
QUALITY_DEFAULT = None
STATUS_DEFAULT = None
WANTED_BEGIN_DEFAULT = None
WANTED_LATEST_DEFAULT = None
FLATTEN_FOLDERS_DEFAULT = False
SUBTITLES_DEFAULT = False
INDEXER_DEFAULT = None
INDEXER_TIMEOUT = None
SCENE_DEFAULT = False
ANIME_DEFAULT = False
USE_IMDB_INFO = True
PROVIDER_ORDER = []
NAMING_MULTI_EP = False
@ -280,6 +288,18 @@ XBMC_HOST = ''
XBMC_USERNAME = None
XBMC_PASSWORD = None
USE_KODI = False
KODI_ALWAYS_ON = True
KODI_NOTIFY_ONSNATCH = False
KODI_NOTIFY_ONDOWNLOAD = False
KODI_NOTIFY_ONSUBTITLEDOWNLOAD = False
KODI_UPDATE_LIBRARY = False
KODI_UPDATE_FULL = False
KODI_UPDATE_ONLYFIRST = False
KODI_HOST = ''
KODI_USERNAME = None
KODI_PASSWORD = None
USE_PLEX = False
PLEX_NOTIFY_ONSNATCH = False
PLEX_NOTIFY_ONDOWNLOAD = False
@ -325,6 +345,9 @@ PUSHOVER_NOTIFY_ONDOWNLOAD = False
PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD = False
PUSHOVER_USERKEY = None
PUSHOVER_APIKEY = None
PUSHOVER_PRIORITY = 0
PUSHOVER_DEVICE = None
PUSHOVER_SOUND = None
USE_LIBNOTIFY = False
LIBNOTIFY_NOTIFY_ONSNATCH = False
@ -336,7 +359,6 @@ NMJ_HOST = None
NMJ_DATABASE = None
NMJ_MOUNT = None
ANIMESUPPORT = False
USE_ANIDB = False
ANIDB_USERNAME = None
ANIDB_PASSWORD = None
@ -396,8 +418,8 @@ USE_PUSHBULLET = False
PUSHBULLET_NOTIFY_ONSNATCH = False
PUSHBULLET_NOTIFY_ONDOWNLOAD = False
PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = False
PUSHBULLET_API = None
PUSHBULLET_DEVICE = None
PUSHBULLET_ACCESS_TOKEN = None
PUSHBULLET_DEVICE_IDEN = None
USE_EMAIL = False
EMAIL_NOTIFY_ONSNATCH = False
@ -412,6 +434,7 @@ EMAIL_FROM = None
EMAIL_LIST = None
GUI_NAME = None
DEFAULT_HOME = None
HOME_LAYOUT = None
HISTORY_LAYOUT = None
DISPLAY_SHOW_SPECIALS = False
@ -463,25 +486,26 @@ def get_backlog_cycle_time():
def initialize(consoleLogging=True):
with INIT_LOCK:
global BRANCH, GIT_REMOTE, CUR_COMMIT_HASH, CUR_COMMIT_BRANCH, ACTUAL_LOG_DIR, LOG_DIR, WEB_PORT, WEB_LOG, ENCRYPTION_VERSION, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, USE_API, API_KEY, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
global BRANCH, GIT_REMOTE, CUR_COMMIT_HASH, CUR_COMMIT_BRANCH, ACTUAL_LOG_DIR, LOG_DIR, FILE_LOGGING_PRESET, WEB_PORT, WEB_LOG, ENCRYPTION_VERSION, WEB_ROOT, WEB_USERNAME, WEB_PASSWORD, WEB_HOST, WEB_IPV6, USE_API, API_KEY, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, \
HANDLE_REVERSE_PROXY, USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, CHECK_PROPERS_INTERVAL, ALLOW_HIGH_PRIORITY, TORRENT_METHOD, \
SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_HOST, \
NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, NZBGET_HOST, NZBGET_USE_HTTPS, backlogSearchScheduler, \
TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_VERIFY_CERT, \
USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \
XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, BACKLOG_FREQUENCY, \
USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, \
USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_API, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \
USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \
PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \
showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, HOME_SEARCH_FOCUS, SORT_ARTICLE, showList, loadingShowList, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, \
NEWZNAB_DATA, NZBS, NZBS_UID, NZBS_HASH, INDEXER_DEFAULT, INDEXER_TIMEOUT, USENET_RETENTION, TORRENT_DIR, \
QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, RECENTSEARCH_STARTUP, \
QUALITY_DEFAULT, FLATTEN_FOLDERS_DEFAULT, SUBTITLES_DEFAULT, STATUS_DEFAULT, WANTED_BEGIN_DEFAULT, WANTED_LATEST_DEFAULT, RECENTSEARCH_STARTUP, \
GROWL_NOTIFY_ONSNATCH, GROWL_NOTIFY_ONDOWNLOAD, GROWL_NOTIFY_ONSUBTITLEDOWNLOAD, TWITTER_NOTIFY_ONSNATCH, TWITTER_NOTIFY_ONDOWNLOAD, TWITTER_NOTIFY_ONSUBTITLEDOWNLOAD, \
USE_GROWL, GROWL_HOST, GROWL_PASSWORD, USE_PROWL, PROWL_NOTIFY_ONSNATCH, PROWL_NOTIFY_ONDOWNLOAD, PROWL_NOTIFY_ONSUBTITLEDOWNLOAD, PROWL_API, PROWL_PRIORITY, PROG_DIR, \
USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \
USE_NMA, NMA_NOTIFY_ONSNATCH, NMA_NOTIFY_ONDOWNLOAD, NMA_NOTIFY_ONSUBTITLEDOWNLOAD, NMA_API, NMA_PRIORITY, \
USE_PUSHALOT, PUSHALOT_NOTIFY_ONSNATCH, PUSHALOT_NOTIFY_ONDOWNLOAD, PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHALOT_AUTHORIZATIONTOKEN, \
USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_API, PUSHBULLET_DEVICE, \
USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_ACCESS_TOKEN, PUSHBULLET_DEVICE_IDEN, \
versionCheckScheduler, VERSION_NOTIFY, AUTO_UPDATE, NOTIFY_ON_UPDATE, PROCESS_AUTOMATICALLY, UNPACK, CPU_PRESET, \
KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, MIN_RECENTSEARCH_FREQUENCY, DEFAULT_UPDATE_FREQUENCY, MIN_UPDATE_FREQUENCY, UPDATE_FREQUENCY, \
showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TIMEZONE_DISPLAY, \
@ -490,21 +514,21 @@ def initialize(consoleLogging=True):
WOMBLE, OMGWTFNZBS, OMGWTFNZBS_USERNAME, OMGWTFNZBS_APIKEY, providerList, newznabProviderList, torrentRssProviderList, \
EXTRA_SCRIPTS, USE_TWITTER, TWITTER_USERNAME, TWITTER_PASSWORD, TWITTER_PREFIX, RECENTSEARCH_FREQUENCY, \
USE_BOXCAR2, BOXCAR2_ACCESSTOKEN, BOXCAR2_NOTIFY_ONDOWNLOAD, BOXCAR2_NOTIFY_ONSUBTITLEDOWNLOAD, BOXCAR2_NOTIFY_ONSNATCH, BOXCAR2_SOUND, \
USE_PUSHOVER, PUSHOVER_USERKEY, PUSHOVER_APIKEY, PUSHOVER_NOTIFY_ONDOWNLOAD, PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHOVER_NOTIFY_ONSNATCH, \
USE_PUSHOVER, PUSHOVER_USERKEY, PUSHOVER_APIKEY, PUSHOVER_NOTIFY_ONDOWNLOAD, PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHOVER_NOTIFY_ONSNATCH, PUSHOVER_PRIORITY, PUSHOVER_DEVICE, PUSHOVER_SOUND, \
USE_LIBNOTIFY, LIBNOTIFY_NOTIFY_ONSNATCH, LIBNOTIFY_NOTIFY_ONDOWNLOAD, LIBNOTIFY_NOTIFY_ONSUBTITLEDOWNLOAD, USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, USE_NMJv2, NMJv2_HOST, NMJv2_DATABASE, NMJv2_DBLOC, USE_SYNOINDEX, \
USE_SYNOLOGYNOTIFIER, SYNOLOGYNOTIFIER_NOTIFY_ONSNATCH, SYNOLOGYNOTIFIER_NOTIFY_ONDOWNLOAD, SYNOLOGYNOTIFIER_NOTIFY_ONSUBTITLEDOWNLOAD, \
USE_EMAIL, EMAIL_HOST, EMAIL_PORT, EMAIL_TLS, EMAIL_USER, EMAIL_PASSWORD, EMAIL_FROM, EMAIL_NOTIFY_ONSNATCH, EMAIL_NOTIFY_ONDOWNLOAD, EMAIL_NOTIFY_ONSUBTITLEDOWNLOAD, EMAIL_LIST, \
USE_LISTVIEW, METADATA_XBMC, METADATA_XBMC_12PLUS, METADATA_MEDIABROWSER, METADATA_PS3, metadata_provider_dict, \
USE_LISTVIEW, METADATA_XBMC, METADATA_XBMC_12PLUS, METADATA_MEDIABROWSER, METADATA_PS3, METADATA_KODI, metadata_provider_dict, \
NEWZBIN, NEWZBIN_USERNAME, NEWZBIN_PASSWORD, GIT_PATH, MOVE_ASSOCIATED_FILES, POSTPONE_IF_SYNC_FILES, recentSearchScheduler, NFO_RENAME, \
GUI_NAME, HOME_LAYOUT, HISTORY_LAYOUT, DISPLAY_SHOW_SPECIALS, EPISODE_VIEW_LAYOUT, EPISODE_VIEW_SORT, EPISODE_VIEW_DISPLAY_PAUSED, EPISODE_VIEW_MISSED_RANGE, FUZZY_DATING, TRIM_ZERO, DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, THEME_NAME, \
GUI_NAME, DEFAULT_HOME, HOME_LAYOUT, HISTORY_LAYOUT, DISPLAY_SHOW_SPECIALS, EPISODE_VIEW_LAYOUT, EPISODE_VIEW_SORT, EPISODE_VIEW_DISPLAY_PAUSED, EPISODE_VIEW_MISSED_RANGE, FUZZY_DATING, TRIM_ZERO, DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, THEME_NAME, \
POSTER_SORTBY, POSTER_SORTDIR, \
METADATA_WDTV, METADATA_TIVO, METADATA_MEDE8ER, IGNORE_WORDS, REQUIRE_WORDS, CALENDAR_UNPROTECTED, CREATE_MISSING_SHOW_DIRS, \
ADD_SHOWS_WO_DIR, USE_SUBTITLES, SUBTITLES_LANGUAGES, SUBTITLES_DIR, SUBTITLES_SERVICES_LIST, SUBTITLES_SERVICES_ENABLED, SUBTITLES_HISTORY, SUBTITLES_FINDER_FREQUENCY, subtitlesFinderScheduler, \
USE_FAILED_DOWNLOADS, DELETE_FAILED, ANON_REDIRECT, LOCALHOST_IP, TMDB_API_KEY, DEBUG, PROXY_SETTING, PROXY_INDEXERS, \
AUTOPOSTPROCESSER_FREQUENCY, DEFAULT_AUTOPOSTPROCESSER_FREQUENCY, MIN_AUTOPOSTPROCESSER_FREQUENCY, \
ANIME_DEFAULT, NAMING_ANIME, ANIMESUPPORT, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \
ANIME_DEFAULT, NAMING_ANIME, USE_ANIDB, ANIDB_USERNAME, ANIDB_PASSWORD, ANIDB_USE_MYLIST, \
ANIME_SPLIT_HOME, SCENE_DEFAULT, BACKLOG_DAYS, ANIME_TREAT_AS_HDTV, \
COOKIE_SECRET
COOKIE_SECRET, USE_IMDB_INFO, DISPLAY_BACKGROUND, DISPLAY_BACKGROUND_TRANSPARENT, DISPLAY_ALL_SEASONS
if __INITIALIZED__:
return False
@ -515,6 +539,7 @@ def initialize(consoleLogging=True):
CheckSection(CFG, 'SABnzbd')
CheckSection(CFG, 'NZBget')
CheckSection(CFG, 'XBMC')
CheckSection(CFG, 'Kodi')
CheckSection(CFG, 'PLEX')
CheckSection(CFG, 'Growl')
CheckSection(CFG, 'Prowl')
@ -562,9 +587,21 @@ def initialize(consoleLogging=True):
if CACHE_DIR:
helpers.clearCache()
GUI_NAME = check_setting_str(CFG, 'GUI', 'gui_name', 'slick')
THEME_NAME = check_setting_str(CFG, 'GUI', 'theme_name', 'dark')
GUI_NAME = check_setting_str(CFG, 'GUI', 'gui_name', 'slick')
DEFAULT_HOME = check_setting_str(CFG, 'GUI', 'default_home', 'home')
USE_IMDB_INFO = bool(check_setting_int(CFG, 'GUI', 'use_imdb_info', 1))
HOME_SEARCH_FOCUS = bool(check_setting_int(CFG, 'General', 'home_search_focus', HOME_SEARCH_FOCUS))
SORT_ARTICLE = bool(check_setting_int(CFG, 'General', 'sort_article', 0))
FUZZY_DATING = bool(check_setting_int(CFG, 'GUI', 'fuzzy_dating', 0))
TRIM_ZERO = bool(check_setting_int(CFG, 'GUI', 'trim_zero', 0))
DATE_PRESET = check_setting_str(CFG, 'GUI', 'date_preset', '%x')
TIME_PRESET_W_SECONDS = check_setting_str(CFG, 'GUI', 'time_preset', '%I:%M:%S %p')
TIME_PRESET = TIME_PRESET_W_SECONDS.replace(u":%S", u"")
TIMEZONE_DISPLAY = check_setting_str(CFG, 'GUI', 'timezone_display', 'network')
DISPLAY_BACKGROUND = bool(check_setting_int(CFG, 'General', 'display_background', 0))
DISPLAY_BACKGROUND_TRANSPARENT = check_setting_str(CFG, 'General', 'display_background_transparent', 'transparent')
DISPLAY_ALL_SEASONS = bool(check_setting_int(CFG, 'General', 'display_all_seasons', 1))
ACTUAL_LOG_DIR = check_setting_str(CFG, 'General', 'log_dir', 'Logs')
# put the log dir inside the data dir, unless an absolute path
@ -573,6 +610,8 @@ def initialize(consoleLogging=True):
if not helpers.makeDir(LOG_DIR):
logger.log(u"!!! No log folder, logging to screen only!", logger.ERROR)
FILE_LOGGING_PRESET = check_setting_str(CFG, 'General', 'file_logging_preset', 'DB')
SOCKET_TIMEOUT = check_setting_int(CFG, 'General', 'socket_timeout', 30)
socket.setdefaulttimeout(SOCKET_TIMEOUT)
@ -610,9 +649,6 @@ def initialize(consoleLogging=True):
TRASH_REMOVE_SHOW = bool(check_setting_int(CFG, 'General', 'trash_remove_show', 0))
TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0))
HOME_SEARCH_FOCUS = bool(check_setting_int(CFG, 'General', 'home_search_focus', HOME_SEARCH_FOCUS))
SORT_ARTICLE = bool(check_setting_int(CFG, 'General', 'sort_article', 0))
USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0))
API_KEY = check_setting_str(CFG, 'General', 'api_key', '')
@ -631,6 +667,8 @@ def initialize(consoleLogging=True):
QUALITY_DEFAULT = check_setting_int(CFG, 'General', 'quality_default', SD)
STATUS_DEFAULT = check_setting_int(CFG, 'General', 'status_default', SKIPPED)
WANTED_BEGIN_DEFAULT = check_setting_int(CFG, 'General', 'wanted_begin_default', 0)
WANTED_LATEST_DEFAULT = check_setting_int(CFG, 'General', 'wanted_latest_default', 0)
VERSION_NOTIFY = bool(check_setting_int(CFG, 'General', 'version_notify', 1))
AUTO_UPDATE = bool(check_setting_int(CFG, 'General', 'auto_update', 0))
NOTIFY_ON_UPDATE = bool(check_setting_int(CFG, 'General', 'notify_on_update', 1))
@ -675,7 +713,7 @@ def initialize(consoleLogging=True):
RECENTSEARCH_STARTUP = bool(check_setting_int(CFG, 'General', 'recentsearch_startup', 0))
BACKLOG_STARTUP = bool(check_setting_int(CFG, 'General', 'backlog_startup', 0))
SKIP_REMOVED_FILES = bool(check_setting_int(CFG, 'General', 'skip_removed_files', 0))
SKIP_REMOVED_FILES = check_setting_int(CFG, 'General', 'skip_removed_files', 0)
USENET_RETENTION = check_setting_int(CFG, 'General', 'usenet_retention', 500)
@ -759,6 +797,18 @@ def initialize(consoleLogging=True):
XBMC_USERNAME = check_setting_str(CFG, 'XBMC', 'xbmc_username', '')
XBMC_PASSWORD = check_setting_str(CFG, 'XBMC', 'xbmc_password', '')
USE_KODI = bool(check_setting_int(CFG, 'Kodi', 'use_kodi', 0))
KODI_ALWAYS_ON = bool(check_setting_int(CFG, 'Kodi', 'kodi_always_on', 1))
KODI_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Kodi', 'kodi_notify_onsnatch', 0))
KODI_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Kodi', 'kodi_notify_ondownload', 0))
KODI_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Kodi', 'kodi_notify_onsubtitledownload', 0))
KODI_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Kodi', 'kodi_update_library', 0))
KODI_UPDATE_FULL = bool(check_setting_int(CFG, 'Kodi', 'kodi_update_full', 0))
KODI_UPDATE_ONLYFIRST = bool(check_setting_int(CFG, 'Kodi', 'kodi_update_onlyfirst', 0))
KODI_HOST = check_setting_str(CFG, 'Kodi', 'kodi_host', '')
KODI_USERNAME = check_setting_str(CFG, 'Kodi', 'kodi_username', '')
KODI_PASSWORD = check_setting_str(CFG, 'Kodi', 'kodi_password', '')
USE_PLEX = bool(check_setting_int(CFG, 'Plex', 'use_plex', 0))
PLEX_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsnatch', 0))
PLEX_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_ondownload', 0))
@ -807,6 +857,10 @@ def initialize(consoleLogging=True):
check_setting_int(CFG, 'Pushover', 'pushover_notify_onsubtitledownload', 0))
PUSHOVER_USERKEY = check_setting_str(CFG, 'Pushover', 'pushover_userkey', '')
PUSHOVER_APIKEY = check_setting_str(CFG, 'Pushover', 'pushover_apikey', '')
PUSHOVER_PRIORITY = check_setting_int(CFG, 'Pushover', 'pushover_priority', 0)
PUSHOVER_DEVICE = check_setting_str(CFG, 'Pushover', 'pushover_device', 'all')
PUSHOVER_SOUND = check_setting_str(CFG, 'Pushover', 'pushover_sound', 'pushover')
USE_LIBNOTIFY = bool(check_setting_int(CFG, 'Libnotify', 'use_libnotify', 0))
LIBNOTIFY_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_onsnatch', 0))
LIBNOTIFY_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Libnotify', 'libnotify_notify_ondownload', 0))
@ -875,8 +929,8 @@ def initialize(consoleLogging=True):
PUSHBULLET_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_ondownload', 0))
PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = bool(
check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_onsubtitledownload', 0))
PUSHBULLET_API = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '')
PUSHBULLET_DEVICE = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '')
PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_access_token', '')
PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device_iden', '')
USE_EMAIL = bool(check_setting_int(CFG, 'Email', 'use_email', 0))
EMAIL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Email', 'email_notify_onsnatch', 0))
@ -918,7 +972,6 @@ def initialize(consoleLogging=True):
USE_LISTVIEW = bool(check_setting_int(CFG, 'General', 'use_listview', 0))
ANIMESUPPORT = False
USE_ANIDB = bool(check_setting_int(CFG, 'ANIDB', 'use_anidb', 0))
ANIDB_USERNAME = check_setting_str(CFG, 'ANIDB', 'anidb_username', '')
ANIDB_PASSWORD = check_setting_str(CFG, 'ANIDB', 'anidb_password', '')
@ -934,6 +987,7 @@ def initialize(consoleLogging=True):
METADATA_WDTV = check_setting_str(CFG, 'General', 'metadata_wdtv', '0|0|0|0|0|0|0|0|0|0')
METADATA_TIVO = check_setting_str(CFG, 'General', 'metadata_tivo', '0|0|0|0|0|0|0|0|0|0')
METADATA_MEDE8ER = check_setting_str(CFG, 'General', 'metadata_mede8er', '0|0|0|0|0|0|0|0|0|0')
METADATA_KODI = check_setting_str(CFG, 'General', 'metadata_kodi', '0|0|0|0|0|0|0|0|0|0')
HOME_LAYOUT = check_setting_str(CFG, 'GUI', 'home_layout', 'poster')
HISTORY_LAYOUT = check_setting_str(CFG, 'GUI', 'history_layout', 'detailed')
@ -942,12 +996,7 @@ def initialize(consoleLogging=True):
EPISODE_VIEW_SORT = check_setting_str(CFG, 'GUI', 'episode_view_sort', 'time')
EPISODE_VIEW_DISPLAY_PAUSED = bool(check_setting_int(CFG, 'GUI', 'episode_view_display_paused', 0))
EPISODE_VIEW_MISSED_RANGE = check_setting_int(CFG, 'GUI', 'episode_view_missed_range', 7)
FUZZY_DATING = bool(check_setting_int(CFG, 'GUI', 'fuzzy_dating', 0))
TRIM_ZERO = bool(check_setting_int(CFG, 'GUI', 'trim_zero', 0))
DATE_PRESET = check_setting_str(CFG, 'GUI', 'date_preset', '%x')
TIME_PRESET_W_SECONDS = check_setting_str(CFG, 'GUI', 'time_preset', '%I:%M:%S %p')
TIME_PRESET = TIME_PRESET_W_SECONDS.replace(u":%S", u"")
TIMEZONE_DISPLAY = check_setting_str(CFG, 'GUI', 'timezone_display', 'network')
POSTER_SORTBY = check_setting_str(CFG, 'GUI', 'poster_sortby', 'name')
POSTER_SORTDIR = check_setting_int(CFG, 'GUI', 'poster_sortdir', 1)
@ -1088,6 +1137,7 @@ def initialize(consoleLogging=True):
(METADATA_WDTV, metadata.wdtv),
(METADATA_TIVO, metadata.tivo),
(METADATA_MEDE8ER, metadata.mede8er),
(METADATA_KODI, metadata.kodi),
]:
(cur_metadata_config, cur_metadata_class) = cur_metadata_tuple
tmp_provider = cur_metadata_class.metadata_class()
@ -1365,6 +1415,7 @@ def save_config():
new_config['General']['config_version'] = CONFIG_VERSION
new_config['General']['encryption_version'] = int(ENCRYPTION_VERSION)
new_config['General']['log_dir'] = ACTUAL_LOG_DIR if ACTUAL_LOG_DIR else 'Logs'
new_config['General']['file_logging_preset'] = FILE_LOGGING_PRESET if FILE_LOGGING_PRESET else 'DB'
new_config['General']['socket_timeout'] = SOCKET_TIMEOUT
new_config['General']['web_port'] = WEB_PORT
new_config['General']['web_host'] = WEB_HOST
@ -1400,6 +1451,8 @@ def save_config():
new_config['General']['skip_removed_files'] = int(SKIP_REMOVED_FILES)
new_config['General']['quality_default'] = int(QUALITY_DEFAULT)
new_config['General']['status_default'] = int(STATUS_DEFAULT)
new_config['General']['wanted_begin_default'] = int(WANTED_BEGIN_DEFAULT)
new_config['General']['wanted_latest_default'] = int(WANTED_LATEST_DEFAULT)
new_config['General']['flatten_folders_default'] = int(FLATTEN_FOLDERS_DEFAULT)
new_config['General']['indexer_default'] = int(INDEXER_DEFAULT)
new_config['General']['indexer_timeout'] = int(INDEXER_TIMEOUT)
@ -1429,6 +1482,9 @@ def save_config():
new_config['General']['sort_article'] = int(SORT_ARTICLE)
new_config['General']['proxy_setting'] = PROXY_SETTING
new_config['General']['proxy_indexers'] = int(PROXY_INDEXERS)
new_config['General']['display_background'] = int(DISPLAY_BACKGROUND)
new_config['General']['display_background_transparent'] = DISPLAY_BACKGROUND_TRANSPARENT
new_config['General']['display_all_seasons'] = int(DISPLAY_ALL_SEASONS)
new_config['General']['use_listview'] = int(USE_LISTVIEW)
new_config['General']['metadata_xbmc'] = METADATA_XBMC
@ -1438,6 +1494,7 @@ def save_config():
new_config['General']['metadata_wdtv'] = METADATA_WDTV
new_config['General']['metadata_tivo'] = METADATA_TIVO
new_config['General']['metadata_mede8er'] = METADATA_MEDE8ER
new_config['General']['metadata_kodi'] = METADATA_KODI
new_config['General']['backlog_days'] = int(BACKLOG_DAYS)
@ -1600,6 +1657,19 @@ def save_config():
new_config['XBMC']['xbmc_username'] = XBMC_USERNAME
new_config['XBMC']['xbmc_password'] = helpers.encrypt(XBMC_PASSWORD, ENCRYPTION_VERSION)
new_config['Kodi'] = {}
new_config['Kodi']['use_kodi'] = int(USE_KODI)
new_config['Kodi']['kodi_always_on'] = int(KODI_ALWAYS_ON)
new_config['Kodi']['kodi_notify_onsnatch'] = int(KODI_NOTIFY_ONSNATCH)
new_config['Kodi']['kodi_notify_ondownload'] = int(KODI_NOTIFY_ONDOWNLOAD)
new_config['Kodi']['kodi_notify_onsubtitledownload'] = int(KODI_NOTIFY_ONSUBTITLEDOWNLOAD)
new_config['Kodi']['kodi_update_library'] = int(KODI_UPDATE_LIBRARY)
new_config['Kodi']['kodi_update_full'] = int(KODI_UPDATE_FULL)
new_config['Kodi']['kodi_update_onlyfirst'] = int(KODI_UPDATE_ONLYFIRST)
new_config['Kodi']['kodi_host'] = KODI_HOST
new_config['Kodi']['kodi_username'] = KODI_USERNAME
new_config['Kodi']['kodi_password'] = helpers.encrypt(KODI_PASSWORD, ENCRYPTION_VERSION)
new_config['Plex'] = {}
new_config['Plex']['use_plex'] = int(USE_PLEX)
new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH)
@ -1651,6 +1721,9 @@ def save_config():
new_config['Pushover']['pushover_notify_onsubtitledownload'] = int(PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD)
new_config['Pushover']['pushover_userkey'] = PUSHOVER_USERKEY
new_config['Pushover']['pushover_apikey'] = PUSHOVER_APIKEY
new_config['Pushover']['pushover_priority'] = int(PUSHOVER_PRIORITY)
new_config['Pushover']['pushover_device'] = PUSHOVER_DEVICE
new_config['Pushover']['pushover_sound'] = PUSHOVER_SOUND
new_config['Libnotify'] = {}
new_config['Libnotify']['use_libnotify'] = int(USE_LIBNOTIFY)
@ -1724,8 +1797,8 @@ def save_config():
new_config['Pushbullet']['pushbullet_notify_onsnatch'] = int(PUSHBULLET_NOTIFY_ONSNATCH)
new_config['Pushbullet']['pushbullet_notify_ondownload'] = int(PUSHBULLET_NOTIFY_ONDOWNLOAD)
new_config['Pushbullet']['pushbullet_notify_onsubtitledownload'] = int(PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD)
new_config['Pushbullet']['pushbullet_api'] = PUSHBULLET_API
new_config['Pushbullet']['pushbullet_device'] = PUSHBULLET_DEVICE
new_config['Pushbullet']['pushbullet_access_token'] = PUSHBULLET_ACCESS_TOKEN
new_config['Pushbullet']['pushbullet_device_iden'] = PUSHBULLET_DEVICE_IDEN
new_config['Email'] = {}
new_config['Email']['use_email'] = int(USE_EMAIL)
@ -1749,18 +1822,20 @@ def save_config():
new_config['GUI'] = {}
new_config['GUI']['gui_name'] = GUI_NAME
new_config['GUI']['theme_name'] = THEME_NAME
new_config['GUI']['home_layout'] = HOME_LAYOUT
new_config['GUI']['history_layout'] = HISTORY_LAYOUT
new_config['GUI']['display_show_specials'] = int(DISPLAY_SHOW_SPECIALS)
new_config['GUI']['episode_view_layout'] = EPISODE_VIEW_LAYOUT
new_config['GUI']['episode_view_display_paused'] = int(EPISODE_VIEW_DISPLAY_PAUSED)
new_config['GUI']['episode_view_sort'] = EPISODE_VIEW_SORT
new_config['GUI']['episode_view_missed_range'] = int(EPISODE_VIEW_MISSED_RANGE)
new_config['GUI']['default_home'] = DEFAULT_HOME
new_config['GUI']['use_imdb_info'] = int(USE_IMDB_INFO)
new_config['GUI']['fuzzy_dating'] = int(FUZZY_DATING)
new_config['GUI']['trim_zero'] = int(TRIM_ZERO)
new_config['GUI']['date_preset'] = DATE_PRESET
new_config['GUI']['time_preset'] = TIME_PRESET_W_SECONDS
new_config['GUI']['timezone_display'] = TIMEZONE_DISPLAY
new_config['GUI']['home_layout'] = HOME_LAYOUT
new_config['GUI']['history_layout'] = HISTORY_LAYOUT
new_config['GUI']['display_show_specials'] = int(DISPLAY_SHOW_SPECIALS)
new_config['GUI']['episode_view_layout'] = EPISODE_VIEW_LAYOUT
new_config['GUI']['episode_view_sort'] = EPISODE_VIEW_SORT
new_config['GUI']['episode_view_display_paused'] = int(EPISODE_VIEW_DISPLAY_PAUSED)
new_config['GUI']['episode_view_missed_range'] = int(EPISODE_VIEW_MISSED_RANGE)
new_config['GUI']['poster_sortby'] = POSTER_SORTBY
new_config['GUI']['poster_sortdir'] = POSTER_SORTDIR

View file

@ -19,26 +19,30 @@
import os.path
import sickbeard
from sickbeard import logger
from sickbeard import logger, processTV
from sickbeard import encodingKludge as ek
from sickbeard import processTV
class PostProcesser():
def run(self, force=False):
def __init__(self):
self.amActive = False
def run(self):
if not sickbeard.PROCESS_AUTOMATICALLY:
return
self.amActive = True
if not ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR):
logger.log(u"Automatic post-processing attempted but dir " + sickbeard.TV_DOWNLOAD_DIR + " doesn't exist",
logger.log(u"Automatic post-processing attempted but dir %s doesn't exist" % sickbeard.TV_DOWNLOAD_DIR,
logger.ERROR)
return
if not ek.ek(os.path.isabs, sickbeard.TV_DOWNLOAD_DIR):
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)
logger.log(u'Automatic post-processing attempted but dir %s is relative '
'(and probably not what you really want to process)' % sickbeard.TV_DOWNLOAD_DIR, logger.ERROR)
return
processTV.processDir(sickbeard.TV_DOWNLOAD_DIR)
def __del__(self):
pass
self.amActive = False

View file

@ -11,7 +11,7 @@
# SickGear 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.
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
@ -19,10 +19,9 @@
import os
import string
from tornado.httputil import HTTPHeaders
from tornado.web import RequestHandler
from sickbeard import encodingKludge as ek
from sickbeard import logger, webserve
from sickbeard import logger
# use the built-in if it's available (python 2.6), if not use the included library
try:
@ -34,6 +33,7 @@ except ImportError:
if os.name == 'nt':
from ctypes import windll
# adapted from http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490
def getWinDrives():
""" Return list of detected drives """
@ -63,11 +63,11 @@ def foldersAtPath(path, includeParent=False, includeFiles=False):
else:
path = os.path.dirname(path)
if path == "":
if path == '':
if os.name == 'nt':
entries = [{'current_path': 'Root'}]
for letter in getWinDrives():
letterPath = letter + ':\\'
letterPath = '%s:\\' % letter
entries.append({'name': letterPath, 'path': letterPath})
return entries
else:
@ -79,21 +79,23 @@ def foldersAtPath(path, includeParent=False, includeFiles=False):
# if we're at the root then the next step is the meta-node showing our drive letters
if path == parentPath and os.name == 'nt':
parentPath = ""
parentPath = ''
try:
fileList = [{'name': filename, 'path': ek.ek(os.path.join, path, filename)} for filename in ek.ek(os.listdir, path)]
fileList = [{'name': filename, 'path': ek.ek(os.path.join, path, filename)} for filename in
ek.ek(os.listdir, path)]
except OSError, e:
logger.log(u"Unable to open " + path + ": " + repr(e) + " / " + str(e), logger.WARNING)
fileList = [{'name': filename, 'path': ek.ek(os.path.join, parentPath, filename)} for filename in ek.ek(os.listdir, parentPath)]
logger.log(u'Unable to open %s: %r / %s' % (path, e, e), logger.WARNING)
fileList = [{'name': filename, 'path': ek.ek(os.path.join, parentPath, filename)} for filename in
ek.ek(os.listdir, parentPath)]
if not includeFiles:
fileList = filter(lambda entry: ek.ek(os.path.isdir, entry['path']), fileList)
# prune out directories to proect the user from doing stupid things (already lower case the dir to reduce calls)
hideList = ["boot", "bootmgr", "cache", "msocache", "recovery", "$recycle.bin", "recycler",
"system volume information", "temporary internet files"] # windows specific
hideList += [".fseventd", ".spotlight", ".trashes", ".vol", "cachedmessages", "caches", "trash"] # osx specific
hideList = ['boot', 'bootmgr', 'cache', 'msocache', 'recovery', '$recycle.bin', 'recycler',
'system volume information', 'temporary internet files'] # windows specific
hideList += ['.fseventd', '.spotlight', '.trashes', '.vol', 'cachedmessages', 'caches', 'trash'] # osx specific
fileList = filter(lambda entry: entry['name'].lower() not in hideList, fileList)
fileList = sorted(fileList,
@ -101,18 +103,7 @@ def foldersAtPath(path, includeParent=False, includeFiles=False):
entries = [{'current_path': path}]
if includeParent and parentPath != path:
entries.append({'name': "..", 'path': parentPath})
entries.append({'name': '..', 'path': parentPath})
entries.extend(fileList)
return entries
class WebFileBrowser(webserve.MainHandler):
def index(self, path='', includeFiles=False, *args, **kwargs):
self.set_header("Content-Type", "application/json")
return json.dumps(foldersAtPath(path, True, bool(int(includeFiles))))
def complete(self, term, includeFiles=0):
self.set_header("Content-Type", "application/json")
paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term), includeFiles=bool(int(includeFiles))) if 'path' in entry]
return json.dumps(paths)
return entries

View file

@ -1,6 +1,6 @@
import sickbeard
from bs4 import BeautifulSoup
class BS4Parser:
def __init__(self, *args, **kwargs):
self.soup = BeautifulSoup(*args, **kwargs)

View file

@ -16,58 +16,11 @@
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import re
import datetime
import sickbeard
import urllib
import datetime
from lib.dateutil import parser
from common import USER_AGENT, Quality
class SickBeardURLopener(urllib.FancyURLopener):
version = USER_AGENT
class AuthURLOpener(SickBeardURLopener):
"""
URLOpener class that supports http auth without needing interactive password entry.
If the provided username/password don't work it simply fails.
user: username to use for HTTP auth
pw: password to use for HTTP auth
"""
def __init__(self, user, pw):
self.username = user
self.password = pw
# remember if we've tried the username/password before
self.numTries = 0
# call the base class
urllib.FancyURLopener.__init__(self)
def prompt_user_passwd(self, host, realm):
"""
Override this function and instead of prompting just give the
username/password that were provided when the class was instantiated.
"""
# if this is the first try then provide a username/password
if self.numTries == 0:
self.numTries = 1
return (self.username, self.password)
# if we've tried before then return blank which cancels the request
else:
return ('', '')
# this is pretty much just a hack for convenience
def openit(self, url):
self.numTries = 0
return SickBeardURLopener.open(self, url)
from sickbeard.common import Quality
class SearchResult:
@ -82,7 +35,7 @@ class SearchResult:
self.show = None
# URL to the NZB/torrent file
self.url = ""
self.url = ''
# used by some providers to store extra info associated with the result
self.extraInfo = []
@ -94,58 +47,57 @@ class SearchResult:
self.quality = Quality.UNKNOWN
# release name
self.name = ""
self.name = ''
# size of the release (-1 = n/a)
self.size = -1
# release group
self.release_group = ""
self.release_group = ''
# version
self.version = -1
def __str__(self):
if self.provider == None:
return "Invalid provider, unable to print self"
if self.provider is None:
return 'Invalid provider, unable to print self'
myString = self.provider.name + " @ " + self.url + "\n"
myString += "Extra Info:\n"
myString = '%s @ %s\n' % (self.provider.name, self.url)
myString += 'Extra Info:\n'
for extra in self.extraInfo:
myString += " " + extra + "\n"
myString += "Episode: " + str(self.episodes) + "\n"
myString += "Quality: " + Quality.qualityStrings[self.quality] + "\n"
myString += "Name: " + self.name + "\n"
myString += "Size: " + str(self.size) + "\n"
myString += "Release Group: " + str(self.release_group) + "\n"
myString += ' %s\n' % extra
myString += 'Episode: %s\n' % self.episodes
myString += 'Quality: %s\n' % Quality.qualityStrings[self.quality]
myString += 'Name: %s\n' % self.name
myString += 'Size: %s\n' % str(self.size)
myString += 'Release Group: %s\n' % self.release_group
return myString
def fileName(self):
return self.episodes[0].prettyName() + "." + self.resultType
return self.episodes[0].prettyName() + '.' + self.resultType
class NZBSearchResult(SearchResult):
"""
Regular NZB result with an URL to the NZB
"""
resultType = "nzb"
resultType = 'nzb'
class NZBDataSearchResult(SearchResult):
"""
NZB result where the actual NZB XML data is stored in the extraInfo
"""
resultType = "nzbdata"
resultType = 'nzbdata'
class TorrentSearchResult(SearchResult):
"""
Torrent result with an URL to the torrent
"""
resultType = "torrent"
resultType = 'torrent'
# torrent hash
content = None
@ -185,9 +137,9 @@ class AllShowsListUI:
if searchterm.lower() in name.lower():
if 'firstaired' not in curShow:
curShow['firstaired'] = str(datetime.date.fromordinal(1))
curShow['firstaired'] = re.sub("([-]0{2}){1,}", "", curShow['firstaired'])
curShow['firstaired'] = re.sub('([-]0{2}){1,}', '', curShow['firstaired'])
fixDate = parser.parse(curShow['firstaired'], fuzzy=True).date()
curShow['firstaired'] = fixDate.strftime("%Y-%m-%d")
curShow['firstaired'] = fixDate.strftime('%Y-%m-%d')
if curShow not in searchResults:
searchResults += [curShow]
@ -238,8 +190,8 @@ class Proper:
self.scene_episode = -1
def __str__(self):
return str(self.date) + " " + self.name + " " + str(self.season) + "x" + str(self.episode) + " of " + str(
self.indexerid) + " from " + str(sickbeard.indexerApi(self.indexer).name)
return str(self.date) + ' ' + self.name + ' ' + str(self.season) + 'x' + str(self.episode) + ' of ' + str(
self.indexerid) + ' from ' + str(sickbeard.indexerApi(self.indexer).name)
class ErrorViewer():

View file

@ -22,44 +22,39 @@ import platform
import re
import uuid
import logger
import sickbeard
import logger
INSTANCE_ID = str(uuid.uuid1())
USER_AGENT = ('SickGear/(' + platform.system() + '; ' + platform.release() + '; ' + INSTANCE_ID + ')')
mediaExtensions = ['avi', 'mkv', 'mpg', 'mpeg', 'wmv',
'ogm', 'mp4', 'iso', 'img', 'divx',
'm2ts', 'm4v', 'ts', 'flv', 'f4v',
'mov', 'rmvb', 'vob', 'dvr-ms', 'wtv',
'ogv', '3gp', 'webm']
USER_AGENT = ('SickGear/(%s; %s; %s)' % (platform.system(), platform.release(), INSTANCE_ID))
mediaExtensions = ['avi', 'mkv', 'mpg', 'mpeg', 'wmv', 'ogm', 'mp4', 'iso', 'img', 'divx', 'm2ts', 'm4v', 'ts', 'flv',
'f4v', 'mov', 'rmvb', 'vob', 'dvr-ms', 'wtv', 'ogv', '3gp', 'webm']
subtitleExtensions = ['srt', 'sub', 'ass', 'idx', 'ssa']
cpu_presets = {'HIGH': 0.1,
'NORMAL': 0.05,
'LOW': 0.01
}
cpu_presets = {'LOW': 0.01, 'NORMAL': 0.05, 'HIGH': 0.1}
### Other constants
# Other constants
MULTI_EP_RESULT = -1
SEASON_RESULT = -2
### Notification Types
# Notification Types
NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2
NOTIFY_SUBTITLE_DOWNLOAD = 3
NOTIFY_GIT_UPDATE = 4
NOTIFY_GIT_UPDATE_TEXT = 5
notifyStrings = {}
notifyStrings[NOTIFY_SNATCH] = "Started Download"
notifyStrings[NOTIFY_DOWNLOAD] = "Download Finished"
notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD] = "Subtitle Download Finished"
notifyStrings[NOTIFY_GIT_UPDATE] = "SickGear Updated"
notifyStrings[NOTIFY_GIT_UPDATE_TEXT] = "SickGear Updated To Commit#: "
notifyStrings = {NOTIFY_SNATCH: 'Started Download',
NOTIFY_DOWNLOAD: 'Download Finished',
NOTIFY_SUBTITLE_DOWNLOAD: 'Subtitle Download Finished',
NOTIFY_GIT_UPDATE: 'SickGear Updated',
NOTIFY_GIT_UPDATE_TEXT: 'SickGear Updated To Commit#: '}
### Episode statuses
# Episode statuses
UNKNOWN = -1 # should never happen
UNAIRED = 1 # episodes that haven't aired yet
SNATCHED = 2 # qualified with quality
@ -70,7 +65,7 @@ ARCHIVED = 6 # episodes that you don't have locally (counts toward download com
IGNORED = 7 # episodes that you don't want included in your download stats
SNATCHED_PROPER = 9 # qualified with quality
SUBTITLED = 10 # qualified with quality
FAILED = 11 #episode downloaded or snatched we don't want
FAILED = 11 # episode downloaded or snatched we don't want
SNATCHED_BEST = 12 # episode redownloaded using best quality
NAMING_REPEAT = 1
@ -80,13 +75,12 @@ NAMING_LIMITED_EXTEND = 8
NAMING_SEPARATED_REPEAT = 16
NAMING_LIMITED_EXTEND_E_PREFIXED = 32
multiEpStrings = {}
multiEpStrings[NAMING_REPEAT] = "Repeat"
multiEpStrings[NAMING_SEPARATED_REPEAT] = "Repeat (Separated)"
multiEpStrings[NAMING_DUPLICATE] = "Duplicate"
multiEpStrings[NAMING_EXTEND] = "Extend"
multiEpStrings[NAMING_LIMITED_EXTEND] = "Extend (Limited)"
multiEpStrings[NAMING_LIMITED_EXTEND_E_PREFIXED] = "Extend (Limited, E-prefixed)"
multiEpStrings = {NAMING_REPEAT: 'Repeat',
NAMING_SEPARATED_REPEAT: 'Repeat (Separated)',
NAMING_DUPLICATE: 'Duplicate',
NAMING_EXTEND: 'Extend',
NAMING_LIMITED_EXTEND: 'Extend (Limited)',
NAMING_LIMITED_EXTEND_E_PREFIXED: 'Extend (Limited, E-prefixed)'}
class Quality:
@ -104,30 +98,30 @@ class Quality:
# put these bits at the other end of the spectrum, far enough out that they shouldn't interfere
UNKNOWN = 1 << 15 # 32768
qualityStrings = {NONE: "N/A",
UNKNOWN: "Unknown",
SDTV: "SD TV",
SDDVD: "SD DVD",
HDTV: "HD TV",
RAWHDTV: "RawHD TV",
FULLHDTV: "1080p HD TV",
HDWEBDL: "720p WEB-DL",
FULLHDWEBDL: "1080p WEB-DL",
HDBLURAY: "720p BluRay",
FULLHDBLURAY: "1080p BluRay"}
qualityStrings = {NONE: 'N/A',
UNKNOWN: 'Unknown',
SDTV: 'SD TV',
SDDVD: 'SD DVD',
HDTV: 'HD TV',
RAWHDTV: 'RawHD TV',
FULLHDTV: '1080p HD TV',
HDWEBDL: '720p WEB-DL',
FULLHDWEBDL: '1080p WEB-DL',
HDBLURAY: '720p BluRay',
FULLHDBLURAY: '1080p BluRay'}
statusPrefixes = {DOWNLOADED: "Downloaded",
SNATCHED: "Snatched",
SNATCHED_PROPER: "Snatched (Proper)",
FAILED: "Failed",
SNATCHED_BEST: "Snatched (Best)"}
statusPrefixes = {DOWNLOADED: 'Downloaded',
SNATCHED: 'Snatched',
SNATCHED_PROPER: 'Snatched (Proper)',
FAILED: 'Failed',
SNATCHED_BEST: 'Snatched (Best)'}
@staticmethod
def _getStatusStrings(status):
toReturn = {}
for x in Quality.qualityStrings.keys():
toReturn[Quality.compositeStatus(status, x)] = Quality.statusPrefixes[status] + " (" + \
Quality.qualityStrings[x] + ")"
toReturn[Quality.compositeStatus(status, x)] = '%s (%s)' % (
Quality.statusPrefixes[status], Quality.qualityStrings[x])
return toReturn
@staticmethod
@ -150,7 +144,7 @@ class Quality:
if curQual << 16 & quality:
bestQualities.append(curQual)
return (sorted(anyQualities), sorted(bestQualities))
return sorted(anyQualities), sorted(bestQualities)
@staticmethod
def nameQuality(name, anime=False):
@ -166,7 +160,7 @@ class Quality:
if x == Quality.UNKNOWN:
continue
if x == Quality.NONE: #Last chance
if x == Quality.NONE: # Last chance
return Quality.sceneQuality(name, anime)
regex = '\W' + Quality.qualityStrings[x].replace(' ', '\W') + '\W'
@ -182,14 +176,14 @@ class Quality:
name = os.path.basename(name)
checkName = lambda list, func: func([re.search(x, name, re.I) for x in list])
checkName = lambda quality_list, func: func([re.search(x, name, re.I) for x in quality_list])
if anime:
dvdOptions = checkName(["dvd", "dvdrip"], any)
blueRayOptions = checkName(["bluray", "blu-ray", "BD"], any)
sdOptions = checkName(["360p", "480p", "848x480", "XviD"], any)
hdOptions = checkName(["720p", "1280x720", "960x720"], any)
fullHD = checkName(["1080p", "1920x1080"], any)
dvdOptions = checkName(['dvd', 'dvdrip'], any)
blueRayOptions = checkName(['bluray', 'blu-ray', 'BD'], any)
sdOptions = checkName(['360p', '480p', '848x480', 'XviD'], any)
hdOptions = checkName(['720p', '1280x720', '960x720'], any)
fullHD = checkName(['1080p', '1920x1080'], any)
if sdOptions and not blueRayOptions and not dvdOptions:
return Quality.SDTV
@ -206,44 +200,41 @@ class Quality:
elif blueRayOptions and fullHD and not hdOptions:
return Quality.FULLHDBLURAY
elif sickbeard.ANIME_TREAT_AS_HDTV:
logger.log(u'Treating file: ' + name + ' with "unknown" quality as HDTV per user settings',
logger.DEBUG)
logger.log(u'Treating file: %s with "unknown" quality as HDTV per user settings' % name, logger.DEBUG)
return Quality.HDTV
else:
return Quality.UNKNOWN
if checkName(["(pdtv|hdtv|dsr|tvrip).(xvid|x264|h.?264)"], all) and not checkName(["(720|1080)[pi]"], all) and\
not checkName(["hr.ws.pdtv.x264"], any):
if checkName(['(pdtv|hdtv|dsr|tvrip).(xvid|x264|h.?264)'], all) and not checkName(['(720|1080)[pi]'], all) \
and not checkName(['hr.ws.pdtv.x264'], any):
return Quality.SDTV
elif checkName(["web.dl|webrip", "xvid|x264|h.?264"], all) and not checkName(["(720|1080)[pi]"], all):
elif checkName(['web.dl|webrip', 'xvid|x264|h.?264'], all) and not checkName(['(720|1080)[pi]'], all):
return Quality.SDTV
elif checkName(["(dvdrip|b[r|d]rip)(.ws)?.(xvid|divx|x264)"], any) and not checkName(["(720|1080)[pi]"], all):
elif checkName(['(dvdrip|b[r|d]rip)(.ws)?.(xvid|divx|x264)'], any) and not checkName(['(720|1080)[pi]'], all):
return Quality.SDDVD
elif checkName(["720p", "hdtv", "x264"], all) or checkName(["hr.ws.pdtv.x264"], any) and not checkName(
["(1080)[pi]"], all):
elif checkName(['720p', 'hdtv', 'x264'], all) or checkName(['hr.ws.pdtv.x264'], any) \
and not checkName(['(1080)[pi]'], all):
return Quality.HDTV
elif checkName(["720p|1080i", "hdtv", "mpeg-?2"], all) or checkName(["1080[pi].hdtv", "h.?264"], all):
elif checkName(['720p|1080i', 'hdtv', 'mpeg-?2'], all) or checkName(['1080[pi].hdtv', 'h.?264'], all):
return Quality.RAWHDTV
elif checkName(["1080p", "hdtv", "x264"], all):
elif checkName(['1080p', 'hdtv', 'x264'], all):
return Quality.FULLHDTV
elif checkName(["720p", "web.dl|webrip"], all) or checkName(["720p", "itunes", "h.?264"], all):
elif checkName(['720p', 'web.dl|webrip'], all) or checkName(['720p', 'itunes', 'h.?264'], all):
return Quality.HDWEBDL
elif checkName(["1080p", "web.dl|webrip"], all) or checkName(["1080p", "itunes", "h.?264"], all):
elif checkName(['1080p', 'web.dl|webrip'], all) or checkName(['1080p', 'itunes', 'h.?264'], all):
return Quality.FULLHDWEBDL
elif checkName(["720p", "bluray|hddvd|b[r|d]rip", "x264"], all):
elif checkName(['720p', 'bluray|hddvd|b[r|d]rip', 'x264'], all):
return Quality.HDBLURAY
elif checkName(["1080p", "bluray|hddvd|b[r|d]rip", "x264"], all):
elif checkName(['1080p', 'bluray|hddvd|b[r|d]rip', 'x264'], all):
return Quality.FULLHDBLURAY
else:
return Quality.UNKNOWN
@staticmethod
def assumeQuality(name):
if name.lower().endswith((".avi", ".mp4")):
if name.lower().endswith(('.avi', '.mp4')):
return Quality.SDTV
# elif name.lower().endswith(".mkv"):
# return Quality.HDTV
elif name.lower().endswith(".ts"):
elif name.lower().endswith('.ts'):
return Quality.RAWHDTV
else:
return Quality.UNKNOWN
@ -260,13 +251,13 @@ class Quality:
def splitCompositeStatus(status):
"""Returns a tuple containing (status, quality)"""
if status == UNKNOWN:
return (UNKNOWN, Quality.UNKNOWN)
return UNKNOWN, Quality.UNKNOWN
for x in sorted(Quality.qualityStrings.keys(), reverse=True):
if status > x * 100:
return (status - x * 100, x)
return status - x * 100, x
return (status, Quality.NONE)
return status, Quality.NONE
@staticmethod
def statusFromName(name, assume=True, anime=False):
@ -302,27 +293,28 @@ ANY = Quality.combineQualities(
BEST = Quality.combineQualities([Quality.SDTV, Quality.HDTV, Quality.HDWEBDL], [Quality.HDTV])
qualityPresets = (SD, HD, HD720p, HD1080p, ANY)
qualityPresetStrings = {SD: "SD",
HD: "HD",
HD720p: "HD720p",
HD1080p: "HD1080p",
ANY: "Any"}
qualityPresetStrings = {SD: 'SD',
HD: 'HD',
HD720p: 'HD720p',
HD1080p: 'HD1080p',
ANY: 'Any'}
class StatusStrings:
def __init__(self):
self.statusStrings = {UNKNOWN: "Unknown",
UNAIRED: "Unaired",
SNATCHED: "Snatched",
DOWNLOADED: "Downloaded",
SKIPPED: "Skipped",
SNATCHED_PROPER: "Snatched (Proper)",
WANTED: "Wanted",
ARCHIVED: "Archived",
IGNORED: "Ignored",
SUBTITLED: "Subtitled",
FAILED: "Failed",
SNATCHED_BEST: "Snatched (Best)"}
self.statusStrings = {UNKNOWN: 'Unknown',
UNAIRED: 'Unaired',
SNATCHED: 'Snatched',
DOWNLOADED: 'Downloaded',
SKIPPED: 'Skipped',
SNATCHED_PROPER: 'Snatched (Proper)',
WANTED: 'Wanted',
ARCHIVED: 'Archived',
IGNORED: 'Ignored',
SUBTITLED: 'Subtitled',
FAILED: 'Failed',
SNATCHED_BEST: 'Snatched (Best)'}
def __getitem__(self, name):
if name in Quality.DOWNLOADED + Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST:
@ -330,12 +322,13 @@ class StatusStrings:
if quality == Quality.NONE:
return self.statusStrings[status]
else:
return self.statusStrings[status] + " (" + Quality.qualityStrings[quality] + ")"
return '%s (%s)' % (self.statusStrings[status], Quality.qualityStrings[quality])
else:
return self.statusStrings[name] if self.statusStrings.has_key(name) else ''
def has_key(self, name):
return name in self.statusStrings or name in Quality.DOWNLOADED or name in Quality.SNATCHED or name in Quality.SNATCHED_PROPER or name in Quality.SNATCHED_BEST
return name in self.statusStrings or name in Quality.DOWNLOADED or name in Quality.SNATCHED \
or name in Quality.SNATCHED_PROPER or name in Quality.SNATCHED_BEST
statusStrings = StatusStrings()
@ -351,19 +344,13 @@ class Overview:
# For both snatched statuses. Note: SNATCHED/QUAL have same value and break dict.
SNATCHED = SNATCHED_PROPER = SNATCHED_BEST # 9
overviewStrings = {SKIPPED: "skipped",
WANTED: "wanted",
QUAL: "qual",
GOOD: "good",
UNAIRED: "unaired",
SNATCHED: "snatched"}
# Get our xml namespaces correct for lxml
XML_NSMAP = {'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsd': 'http://www.w3.org/2001/XMLSchema'}
overviewStrings = {SKIPPED: 'skipped',
WANTED: 'wanted',
QUAL: 'qual',
GOOD: 'good',
UNAIRED: 'unaired',
SNATCHED: 'snatched'}
countryList = {'Australia': 'AU',
'Canada': 'CA',
'USA': 'US'
}
'USA': 'US'}

View file

@ -20,35 +20,31 @@ import os.path
import datetime
import re
import urlparse
import sickbeard
from sickbeard import encodingKludge as ek
from sickbeard import helpers
from sickbeard import logger
from sickbeard import naming
from sickbeard import db
from sickbeard import providers
from sickbeard.providers.generic import GenericProvider
from sickbeard import helpers, logger, naming, db, providers
naming_ep_type = ("%(seasonnumber)dx%(episodenumber)02d",
"s%(seasonnumber)02de%(episodenumber)02d",
"S%(seasonnumber)02dE%(episodenumber)02d",
"%(seasonnumber)02dx%(episodenumber)02d")
sports_ep_type = ("%(seasonnumber)dx%(episodenumber)02d",
"s%(seasonnumber)02de%(episodenumber)02d",
"S%(seasonnumber)02dE%(episodenumber)02d",
"%(seasonnumber)02dx%(episodenumber)02d")
naming_ep_type = ('%(seasonnumber)dx%(episodenumber)02d',
's%(seasonnumber)02de%(episodenumber)02d',
'S%(seasonnumber)02dE%(episodenumber)02d',
'%(seasonnumber)02dx%(episodenumber)02d')
naming_ep_type_text = ("1x02", "s01e02", "S01E02", "01x02")
sports_ep_type = ('%(seasonnumber)dx%(episodenumber)02d',
's%(seasonnumber)02de%(episodenumber)02d',
'S%(seasonnumber)02dE%(episodenumber)02d',
'%(seasonnumber)02dx%(episodenumber)02d')
naming_multi_ep_type = {0: ["-%(episodenumber)02d"] * len(naming_ep_type),
1: [" - " + x for x in naming_ep_type],
2: [x + "%(episodenumber)02d" for x in ("x", "e", "E", "x")]}
naming_multi_ep_type_text = ("extend", "duplicate", "repeat")
naming_ep_type_text = ('1x02', 's01e02', 'S01E02', '01x02')
naming_sep_type = (" - ", " ")
naming_sep_type_text = (" - ", "space")
naming_multi_ep_type = {0: ['-%(episodenumber)02d'] * len(naming_ep_type),
1: [' - %s' % x for x in naming_ep_type],
2: [x + '%(episodenumber)02d' for x in ('x', 'e', 'E', 'x')]}
naming_multi_ep_type_text = ('extend', 'duplicate', 'repeat')
naming_sep_type = (' - ', ' ')
naming_sep_type_text = (' - ', 'space')
def change_HTTPS_CERT(https_cert):
@ -59,7 +55,7 @@ def change_HTTPS_CERT(https_cert):
if os.path.normpath(sickbeard.HTTPS_CERT) != os.path.normpath(https_cert):
if helpers.makeDir(os.path.dirname(os.path.abspath(https_cert))):
sickbeard.HTTPS_CERT = os.path.normpath(https_cert)
logger.log(u"Changed https cert path to " + https_cert)
logger.log(u'Changed https cert path to %s' % https_cert)
else:
return False
@ -74,7 +70,7 @@ def change_HTTPS_KEY(https_key):
if os.path.normpath(sickbeard.HTTPS_KEY) != os.path.normpath(https_key):
if helpers.makeDir(os.path.dirname(os.path.abspath(https_key))):
sickbeard.HTTPS_KEY = os.path.normpath(https_key)
logger.log(u"Changed https key path to " + https_key)
logger.log(u'Changed https key path to %s' % https_key)
else:
return False
@ -92,13 +88,13 @@ def change_LOG_DIR(log_dir, web_log):
sickbeard.LOG_DIR = abs_log_dir
logger.sb_log_instance.initLogging()
logger.log(u"Initialized new log file in " + sickbeard.LOG_DIR)
logger.log(u'Initialized new log file in %s' % sickbeard.LOG_DIR)
log_dir_changed = True
else:
return False
if sickbeard.WEB_LOG != web_log_value or log_dir_changed == True:
if sickbeard.WEB_LOG != web_log_value or log_dir_changed:
sickbeard.WEB_LOG = web_log_value
return True
@ -112,7 +108,7 @@ def change_NZB_DIR(nzb_dir):
if os.path.normpath(sickbeard.NZB_DIR) != os.path.normpath(nzb_dir):
if helpers.makeDir(nzb_dir):
sickbeard.NZB_DIR = os.path.normpath(nzb_dir)
logger.log(u"Changed NZB folder to " + nzb_dir)
logger.log(u'Changed NZB folder to %s' % nzb_dir)
else:
return False
@ -127,7 +123,7 @@ def change_TORRENT_DIR(torrent_dir):
if os.path.normpath(sickbeard.TORRENT_DIR) != os.path.normpath(torrent_dir):
if helpers.makeDir(torrent_dir):
sickbeard.TORRENT_DIR = os.path.normpath(torrent_dir)
logger.log(u"Changed torrent folder to " + torrent_dir)
logger.log(u'Changed torrent folder to %s' % torrent_dir)
else:
return False
@ -142,7 +138,7 @@ def change_TV_DOWNLOAD_DIR(tv_download_dir):
if os.path.normpath(sickbeard.TV_DOWNLOAD_DIR) != os.path.normpath(tv_download_dir):
if helpers.makeDir(tv_download_dir):
sickbeard.TV_DOWNLOAD_DIR = os.path.normpath(tv_download_dir)
logger.log(u"Changed TV download folder to " + tv_download_dir)
logger.log(u'Changed TV download folder to %s' % tv_download_dir)
else:
return False
@ -157,6 +153,7 @@ def change_AUTOPOSTPROCESSER_FREQUENCY(freq):
sickbeard.autoPostProcesserScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.AUTOPOSTPROCESSER_FREQUENCY)
def change_RECENTSEARCH_FREQUENCY(freq):
sickbeard.RECENTSEARCH_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_RECENTSEARCH_FREQUENCY)
@ -165,6 +162,7 @@ def change_RECENTSEARCH_FREQUENCY(freq):
sickbeard.recentSearchScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.RECENTSEARCH_FREQUENCY)
def change_BACKLOG_FREQUENCY(freq):
sickbeard.BACKLOG_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_BACKLOG_FREQUENCY)
@ -174,6 +172,7 @@ def change_BACKLOG_FREQUENCY(freq):
sickbeard.backlogSearchScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.BACKLOG_FREQUENCY)
def change_UPDATE_FREQUENCY(freq):
sickbeard.UPDATE_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_UPDATE_FREQUENCY)
@ -182,6 +181,7 @@ def change_UPDATE_FREQUENCY(freq):
sickbeard.versionCheckScheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_FREQUENCY)
def change_VERSION_NOTIFY(version_notify):
oldSetting = sickbeard.VERSION_NOTIFY
@ -190,8 +190,9 @@ def change_VERSION_NOTIFY(version_notify):
if not version_notify:
sickbeard.NEWEST_VERSION_STRING = None
if oldSetting == False and version_notify == True:
sickbeard.versionCheckScheduler.action.run() # @UndefinedVariable
if not oldSetting and version_notify:
sickbeard.versionCheckScheduler.action.run()
def change_DOWNLOAD_PROPERS(download_propers):
if sickbeard.DOWNLOAD_PROPERS == download_propers:
@ -202,12 +203,13 @@ def change_DOWNLOAD_PROPERS(download_propers):
sickbeard.properFinderScheduler.start()
else:
sickbeard.properFinderScheduler.stop.set()
logger.log(u"Waiting for the PROPERFINDER thread to exit")
logger.log(u'Waiting for the PROPERFINDER thread to exit')
try:
sickbeard.properFinderScheduler.join(10)
except:
pass
def change_USE_TRAKT(use_trakt):
if sickbeard.USE_TRAKT == use_trakt:
return
@ -217,12 +219,13 @@ def change_USE_TRAKT(use_trakt):
sickbeard.traktCheckerScheduler.start()
else:
sickbeard.traktCheckerScheduler.stop.set()
logger.log(u"Waiting for the TRAKTCHECKER thread to exit")
logger.log(u'Waiting for the TRAKTCHECKER thread to exit')
try:
sickbeard.traktCheckerScheduler.join(10)
except:
pass
def change_USE_SUBTITLES(use_subtitles):
if sickbeard.USE_SUBTITLES == use_subtitles:
return
@ -232,12 +235,13 @@ def change_USE_SUBTITLES(use_subtitles):
sickbeard.subtitlesFinderScheduler.start()
else:
sickbeard.subtitlesFinderScheduler.stop.set()
logger.log(u"Waiting for the SUBTITLESFINDER thread to exit")
logger.log(u'Waiting for the SUBTITLESFINDER thread to exit')
try:
sickbeard.subtitlesFinderScheduler.join(10)
except:
pass
def CheckSection(CFG, sec):
""" Check if INI section exists, if not create it """
try:
@ -281,10 +285,10 @@ def clean_host(host, default_port=None):
if cleaned_host:
if cleaned_port:
host = cleaned_host + ':' + cleaned_port
host = '%s:%s' % (cleaned_host, cleaned_port)
elif default_port:
host = cleaned_host + ':' + str(default_port)
host = '%s:%s' % (cleaned_host, default_port)
else:
host = cleaned_host
@ -298,14 +302,14 @@ def clean_host(host, default_port=None):
def clean_hosts(hosts, default_port=None):
cleaned_hosts = []
for cur_host in [x.strip() for x in hosts.split(",")]:
for cur_host in [x.strip() for x in hosts.split(',')]:
if cur_host:
cleaned_host = clean_host(cur_host, default_port)
if cleaned_host:
cleaned_hosts.append(cleaned_host)
if cleaned_hosts:
cleaned_hosts = ",".join(cleaned_hosts)
cleaned_hosts = ','.join(cleaned_hosts)
else:
cleaned_hosts = ''
@ -314,10 +318,7 @@ def clean_hosts(hosts, default_port=None):
def clean_url(url, add_slash=True):
"""
Returns an cleaned url starting with a scheme and folder with trailing /
or an empty string
"""
""" Returns an cleaned url starting with a scheme and folder with trailing '/' or an empty string """
if url and url.strip():
@ -329,9 +330,9 @@ def clean_url(url, add_slash=True):
scheme, netloc, path, query, fragment = urlparse.urlsplit(url, 'http')
if not path.endswith('/'):
basename, ext = ek.ek(os.path.splitext, ek.ek(os.path.basename, path)) # @UnusedVariable
basename, ext = ek.ek(os.path.splitext, ek.ek(os.path.basename, path))
if not ext and add_slash:
path = path + '/'
path += '/'
cleaned_url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
@ -352,9 +353,6 @@ def to_int(val, default=0):
return val
################################################################################
# Check_setting_int #
################################################################################
def minimax(val, default, low, high):
""" Return value forced within range """
@ -368,9 +366,6 @@ def minimax(val, default, low, high):
return val
################################################################################
# Check_setting_int #
################################################################################
def check_setting_int(config, cfg_name, item_name, def_val):
try:
my_val = int(config[cfg_name][item_name])
@ -381,13 +376,10 @@ def check_setting_int(config, cfg_name, item_name, def_val):
except:
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
logger.log(item_name + " -> " + str(my_val), logger.DEBUG)
logger.log('%s -> %s' % (item_name, my_val), logger.DEBUG)
return my_val
################################################################################
# Check_setting_float #
################################################################################
def check_setting_float(config, cfg_name, item_name, def_val):
try:
my_val = float(config[cfg_name][item_name])
@ -399,15 +391,16 @@ def check_setting_float(config, cfg_name, item_name, def_val):
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
logger.log(item_name + " -> " + str(my_val), logger.DEBUG)
logger.log('%s -> %s' % (item_name, my_val), logger.DEBUG)
return my_val
################################################################################
# Check_setting_str #
################################################################################
def check_setting_str(config, cfg_name, item_name, def_val, log=True):
# For passwords you must include the word `password` in the item_name and add `helpers.encrypt(ITEM_NAME, ENCRYPTION_VERSION)` in save_config()
"""
For passwords you must include the word `password` in the item_name and
add `helpers.encrypt(ITEM_NAME, ENCRYPTION_VERSION)` in save_config()
"""
if bool(item_name.find('password') + 1):
log = False
encryption_version = sickbeard.ENCRYPTION_VERSION
@ -425,17 +418,18 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True):
config[cfg_name][item_name] = helpers.encrypt(my_val, encryption_version)
if log:
logger.log(item_name + " -> " + str(my_val), logger.DEBUG)
logger.log('%s -> %s' % (item_name, my_val), logger.DEBUG)
else:
logger.log(item_name + " -> ******", logger.DEBUG)
logger.log('%s -> ******' % item_name, logger.DEBUG)
return my_val
class ConfigMigrator():
def __init__(self, config_obj):
"""
Initializes a config migrator that can take the config from the version indicated in the config
file up to the version required by SB
file up to the version required by SG
"""
self.config_obj = config_obj
@ -449,19 +443,18 @@ class ConfigMigrator():
4: 'Add newznab catIDs',
5: 'Metadata update',
6: 'Rename daily search to recent search',
7: 'Rename coming episodes to episode view'
}
7: 'Rename coming episodes to episode view',
8: 'Disable searches on start',
9: 'Rename pushbullet variables'}
def migrate_config(self):
"""
Calls each successive migration until the config is the same version as SB expects
"""
""" Calls each successive migration until the config is the same version as SG expects """
if self.config_version > self.expected_config_version:
logger.log_error_and_exit(u"Your config version (" + str(
self.config_version) + ") has been incremented past what this version of SickGear supports (" + str(
self.expected_config_version) + ").\n" + \
"If you have used other forks or a newer version of SickGear, your config file may be unusable due to their modifications.")
logger.log_error_and_exit(
u'Your config version (%s) has been incremented past what this version of SickGear supports (%s).\n'
'If you have used other forks or a newer version of SickGear, your config file may be unusable due to '
'their modifications.' % (self.config_version, self.expected_config_version))
sickbeard.CONFIG_VERSION = self.config_version
@ -469,24 +462,24 @@ class ConfigMigrator():
next_version = self.config_version + 1
if next_version in self.migration_names:
migration_name = ': ' + self.migration_names[next_version]
migration_name = ': %s' % self.migration_names[next_version]
else:
migration_name = ''
logger.log(u"Backing up config before upgrade")
logger.log(u'Backing up config before upgrade')
if not helpers.backupVersionedFile(sickbeard.CONFIG_FILE, self.config_version):
logger.log_error_and_exit(u"Config backup failed, abort upgrading config")
logger.log_error_and_exit(u'Config backup failed, abort upgrading config')
else:
logger.log(u"Proceeding with upgrade")
logger.log(u'Proceeding with upgrade')
# do the migration, expect a method named _migrate_v<num>
logger.log(u"Migrating config up to version " + str(next_version) + migration_name)
getattr(self, '_migrate_v' + str(next_version))()
# do the migration, expect a method named _migrate_v<num>
logger.log(u'Migrating config up to version %s %s' % (next_version, migration_name))
getattr(self, '_migrate_v%s' % next_version)()
self.config_version = next_version
# save new config after migration
sickbeard.CONFIG_VERSION = self.config_version
logger.log(u"Saving config file to disk")
logger.log(u'Saving config file to disk')
sickbeard.save_config()
# Migration v1: Custom naming
@ -496,13 +489,13 @@ class ConfigMigrator():
"""
sickbeard.NAMING_PATTERN = self._name_to_pattern()
logger.log("Based on your old settings I'm setting your new naming pattern to: " + sickbeard.NAMING_PATTERN)
logger.log('Based on your old settings I am setting your new naming pattern to: %s' % sickbeard.NAMING_PATTERN)
sickbeard.NAMING_CUSTOM_ABD = bool(check_setting_int(self.config_obj, 'General', 'naming_dates', 0))
if sickbeard.NAMING_CUSTOM_ABD:
sickbeard.NAMING_ABD_PATTERN = self._name_to_pattern(True)
logger.log("Adding a custom air-by-date naming pattern to your config: " + sickbeard.NAMING_ABD_PATTERN)
logger.log('Adding a custom air-by-date naming pattern to your config: %s' % sickbeard.NAMING_ABD_PATTERN)
else:
sickbeard.NAMING_ABD_PATTERN = naming.name_abd_presets[0]
@ -510,7 +503,7 @@ class ConfigMigrator():
# see if any of their shows used season folders
myDB = db.DBConnection()
season_folder_shows = myDB.select("SELECT * FROM tv_shows WHERE flatten_folders = 0")
season_folder_shows = myDB.select('SELECT * FROM tv_shows WHERE flatten_folders = 0')
# if any shows had season folders on then prepend season folder to the pattern
if season_folder_shows:
@ -523,20 +516,20 @@ class ConfigMigrator():
new_season_format = str(new_season_format).replace('09', '%0S')
new_season_format = new_season_format.replace('9', '%S')
logger.log(
u"Changed season folder format from " + old_season_format + " to " + new_season_format + ", prepending it to your naming config")
logger.log(u'Changed season folder format from %s to %s, prepending it to your naming config' %
(old_season_format, new_season_format))
sickbeard.NAMING_PATTERN = new_season_format + os.sep + sickbeard.NAMING_PATTERN
except (TypeError, ValueError):
logger.log(u"Can't change " + old_season_format + " to new season format", logger.ERROR)
logger.log(u'Can not change %s to new season format' % old_season_format, logger.ERROR)
# if no shows had it on then don't flatten any shows and don't put season folders in the config
else:
logger.log(u"No shows were using season folders before so I'm disabling flattening on all shows")
logger.log(u'No shows were using season folders before so I am disabling flattening on all shows')
# don't flatten any shows at all
myDB.action("UPDATE tv_shows SET flatten_folders = 0")
myDB.action('UPDATE tv_shows SET flatten_folders = 0')
sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders()
@ -552,11 +545,11 @@ class ConfigMigrator():
use_ep_name = bool(check_setting_int(self.config_obj, 'General', 'naming_ep_name', 1))
# make the presets into templates
naming_ep_type = ("%Sx%0E",
"s%0Se%0E",
"S%0SE%0E",
"%0Sx%0E")
naming_sep_type = (" - ", " ")
naming_ep_type = ('%Sx%0E',
's%0Se%0E',
'S%0SE%0E',
'%0Sx%0E')
naming_sep_type = (' - ', ' ')
# set up our data to use
if use_periods:
@ -575,7 +568,7 @@ class ConfigMigrator():
else:
ep_string = naming_ep_type[ep_type]
finalName = ""
finalName = ''
# start with the show name
if use_show_name:
@ -593,7 +586,7 @@ class ConfigMigrator():
finalName += naming_sep_type[sep_type] + ep_quality
if use_periods:
finalName = re.sub("\s+", ".", finalName)
finalName = re.sub('\s+', '.', finalName)
return finalName
@ -618,13 +611,13 @@ class ConfigMigrator():
old_newznab_data = check_setting_str(self.config_obj, 'Newznab', 'newznab_data', '')
if old_newznab_data:
old_newznab_data_list = old_newznab_data.split("!!!")
old_newznab_data_list = old_newznab_data.split('!!!')
for cur_provider_data in old_newznab_data_list:
try:
name, url, key, enabled = cur_provider_data.split("|")
name, url, key, enabled = cur_provider_data.split('|')
except ValueError:
logger.log(u"Skipping Newznab provider string: '" + cur_provider_data + "', incorrect format",
logger.log(u'Skipping Newznab provider string: "%s", incorrect format' % cur_provider_data,
logger.ERROR)
continue
@ -637,15 +630,15 @@ class ConfigMigrator():
catIDs = '5030,5040,5060'
cur_provider_data_list = [name, url, key, catIDs, enabled]
new_newznab_data.append("|".join(cur_provider_data_list))
new_newznab_data.append('|'.join(cur_provider_data_list))
sickbeard.NEWZNAB_DATA = "!!!".join(new_newznab_data)
sickbeard.NEWZNAB_DATA = '!!!'.join(new_newznab_data)
# Migration v5: Metadata upgrade
def _migrate_v5(self):
""" Updates metadata values to the new format """
""" Updates metadata values to the new format
""" Quick overview of what the upgrade does:
Quick overview of what the upgrade does:
new | old | description (new)
----+-----+--------------------
@ -675,6 +668,7 @@ class ConfigMigrator():
metadata_wdtv = check_setting_str(self.config_obj, 'General', 'metadata_wdtv', '0|0|0|0|0|0')
metadata_tivo = check_setting_str(self.config_obj, 'General', 'metadata_tivo', '0|0|0|0|0|0')
metadata_mede8er = check_setting_str(self.config_obj, 'General', 'metadata_mede8er', '0|0|0|0|0|0')
metadata_kodi = check_setting_str(self.config_obj, 'General', 'metadata_kodi', '0|0|0|0|0|0')
use_banner = bool(check_setting_int(self.config_obj, 'General', 'use_banner', 0))
@ -682,30 +676,28 @@ class ConfigMigrator():
cur_metadata = metadata.split('|')
# if target has the old number of values, do upgrade
if len(cur_metadata) == 6:
logger.log(u"Upgrading " + metadata_name + " metadata, old value: " + metadata)
logger.log(u'Upgrading ' + metadata_name + ' metadata, old value: ' + metadata)
cur_metadata.insert(4, '0')
cur_metadata.append('0')
cur_metadata.append('0')
cur_metadata.append('0')
# swap show fanart, show poster
cur_metadata[3], cur_metadata[2] = cur_metadata[2], cur_metadata[3]
# if user was using use_banner to override the poster, instead enable the banner option and deactivate poster
# if user was using use_banner to override the poster,
# instead enable the banner option and deactivate poster
if metadata_name == 'XBMC' and use_banner:
cur_metadata[4], cur_metadata[3] = cur_metadata[3], '0'
# write new format
metadata = '|'.join(cur_metadata)
logger.log(u"Upgrading " + metadata_name + " metadata, new value: " + metadata)
logger.log(u'Upgrading %s metadata, new value: %s' % (metadata_name, metadata))
elif len(cur_metadata) == 10:
metadata = '|'.join(cur_metadata)
logger.log(u"Keeping " + metadata_name + " metadata, value: " + metadata)
logger.log(u'Keeping %s metadata, value: %s' % (metadata_name, metadata))
else:
logger.log(u"Skipping " + metadata_name + " metadata: '" + metadata + "', incorrect format",
logger.ERROR)
logger.log(u'Skipping %s: "%s", incorrect format' % (metadata_name, metadata), logger.ERROR)
metadata = '0|0|0|0|0|0|0|0|0|0'
logger.log(u"Setting " + metadata_name + " metadata, new value: " + metadata)
logger.log(u'Setting %s metadata, new value: %s' % (metadata_name, metadata))
return metadata
@ -716,10 +708,10 @@ class ConfigMigrator():
sickbeard.METADATA_WDTV = _migrate_metadata(metadata_wdtv, 'WDTV', use_banner)
sickbeard.METADATA_TIVO = _migrate_metadata(metadata_tivo, 'TIVO', use_banner)
sickbeard.METADATA_MEDE8ER = _migrate_metadata(metadata_mede8er, 'Mede8er', use_banner)
sickbeard.METADATA_KODI = _migrate_metadata(metadata_kodi, 'Kodi', use_banner)
# Migration v6: Rename daily search to recent search
def _migrate_v6(self):
sickbeard.RECENTSEARCH_FREQUENCY = check_setting_int(self.config_obj, 'General', 'dailysearch_frequency',
sickbeard.DEFAULT_RECENTSEARCH_FREQUENCY)
@ -729,19 +721,23 @@ class ConfigMigrator():
for curProvider in providers.sortedProviderList():
if hasattr(curProvider, 'enable_recentsearch'):
curProvider.enable_recentsearch = bool(check_setting_int(self.config_obj, curProvider.getID().upper(),
curProvider.getID() + '_enable_dailysearch', 1))
curProvider.enable_recentsearch = bool(check_setting_int(
self.config_obj, curProvider.getID().upper(), curProvider.getID() + '_enable_dailysearch', 1))
def _migrate_v7(self):
sickbeard.EPISODE_VIEW_LAYOUT = check_setting_str(self.config_obj, 'GUI', 'coming_eps_layout', 'banner')
sickbeard.EPISODE_VIEW_SORT = check_setting_str(self.config_obj, 'GUI', 'coming_eps_sort', 'time')
if 'date' == sickbeard.EPISODE_VIEW_SORT:
sickbeard.EPISODE_VIEW_SORT = 'time'
sickbeard.EPISODE_VIEW_DISPLAY_PAUSED = bool(check_setting_int(self.config_obj, 'GUI', 'coming_eps_display_paused', 0))
sickbeard.EPISODE_VIEW_DISPLAY_PAUSED = bool(
check_setting_int(self.config_obj, 'GUI', 'coming_eps_display_paused', 0))
sickbeard.EPISODE_VIEW_MISSED_RANGE = check_setting_int(self.config_obj, 'GUI', 'coming_eps_missed_range', 7)
def _migrate_v8(self):
# removing settings from gui and making it a hidden debug option
sickbeard.RECENTSEARCH_STARTUP = False
sickbeard.BACKLOG_STARTUP = False
def _migrate_v9(self):
sickbeard.PUSHBULLET_ACCESS_TOKEN = check_setting_str(self.config_obj, 'Pushbullet', 'pushbullet_api', '')
sickbeard.PUSHBULLET_DEVICE_IDEN = check_setting_str(self.config_obj, 'Pushbullet', 'pushbullet_device', '')

View file

@ -100,3 +100,27 @@ class AddNetworkConversions(AddSceneExceptionsRefresh):
' tvrage_country TEXT)')
self.connection.action('CREATE INDEX tvrage_idx on network_conversions (tvrage_network, tvrage_country)')
class ConsolidateProviders(AddNetworkConversions):
def test(self):
return self.checkDBVersion() > 1
def execute(self):
db.backup_database('cache.db', self.checkDBVersion())
if self.hasTable('provider_cache'):
self.connection.action('DROP TABLE provider_cache')
self.connection.action('CREATE TABLE provider_cache (provider TEXT ,name TEXT, season NUMERIC, episodes TEXT,'
' indexerid NUMERIC, url TEXT UNIQUE, time NUMERIC, quality TEXT, release_group TEXT, '
'version NUMERIC)')
keep_tables = set(['lastUpdate', 'lastSearch', 'db_version', 'scene_exceptions', 'scene_names',
'network_timezones', 'scene_exceptions_refresh', 'network_conversions', 'provider_cache'])
current_tables = set(self.listTables())
remove_tables = list(current_tables - keep_tables)
for table in remove_tables:
self.connection.action('DROP TABLE [%s]' % table)
self.incDBVersion()

View file

@ -27,7 +27,7 @@ from sickbeard import encodingKludge as ek
from sickbeard.name_parser.parser import NameParser, InvalidNameException, InvalidShowException
MIN_DB_VERSION = 9 # oldest db version we support migrating from
MAX_DB_VERSION = 20001
MAX_DB_VERSION = 20002
class MainSanityCheck(db.DBSanityCheck):
@ -145,12 +145,6 @@ class MainSanityCheck(db.DBSanityCheck):
logger.log(u'No UNAIRED episodes, check passed')
def backup_database(version):
logger.log(u'Backing up database before upgrade')
if not helpers.backupVersionedFile(db.dbFilename(), version):
logger.log_error_and_exit(u'Database backup failed, abort upgrading database')
else:
logger.log(u'Proceeding with upgrade')
# ======================
# = Main DB Migrations =
@ -161,7 +155,7 @@ def backup_database(version):
# 0 -> 31
class InitialSchema(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasTable('tv_shows') and not self.hasTable('db_version'):
queries = [
@ -170,8 +164,8 @@ class InitialSchema(db.SchemaUpgrade):
'CREATE TABLE imdb_info (indexer_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)',
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC)',
'CREATE TABLE scene_numbering(indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER,scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY(indexer_id, season, episode))',
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer TEXT, show_name TEXT, location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, air_by_date NUMERIC, lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT, last_update_indexer NUMERIC, dvdorder NUMERIC, archive_firstmatch NUMERIC, rls_require_words TEXT, rls_ignore_words TEXT, sports NUMERIC);',
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC, indexer TEXT, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC, hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT, subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC, scene_season NUMERIC, scene_episode NUMERIC);',
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer NUMERIC, show_name TEXT, location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, air_by_date NUMERIC, lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT, last_update_indexer NUMERIC, dvdorder NUMERIC, archive_firstmatch NUMERIC, rls_require_words TEXT, rls_ignore_words TEXT, sports NUMERIC);',
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC, indexer NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC, hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT, subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC, scene_season NUMERIC, scene_episode NUMERIC);',
'CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id)',
'CREATE INDEX idx_showid ON tv_episodes (showid);',
'CREATE INDEX idx_sta_epi_air ON tv_episodes (status,episode, airdate);',
@ -209,7 +203,7 @@ class InitialSchema(db.SchemaUpgrade):
class AddSizeAndSceneNameFields(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasColumn('tv_episodes', 'file_size'):
self.addColumn('tv_episodes', 'file_size')
@ -320,7 +314,7 @@ class AddSizeAndSceneNameFields(db.SchemaUpgrade):
# 10 -> 11
class RenameSeasonFolders(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
# rename the column
self.connection.action('ALTER TABLE tv_shows RENAME TO tmp_tv_shows')
@ -404,7 +398,7 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
return result
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
# update the default quality so we dont grab the wrong qualities after migration
sickbeard.QUALITY_DEFAULT = self._update_composite_qualities(sickbeard.QUALITY_DEFAULT)
@ -481,7 +475,7 @@ class AddShowidTvdbidIndex(db.SchemaUpgrade):
# Adding index on tvdb_id (tv_shows) and showid (tv_episodes) to speed up searches/queries
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Checking for duplicate shows before adding unique index.')
MainSanityCheck(self.connection).fix_duplicate_shows('tvdb_id')
@ -500,20 +494,22 @@ class AddShowidTvdbidIndex(db.SchemaUpgrade):
class AddLastUpdateTVDB(db.SchemaUpgrade):
# Adding column last_update_tvdb to tv_shows for controlling nightly updates
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column last_update_tvdb to tvshows')
if not self.hasColumn('tv_shows', 'last_update_tvdb'):
logger.log(u'Adding column last_update_tvdb to tv_shows')
self.addColumn('tv_shows', 'last_update_tvdb', default=1)
self.incDBVersion()
return self.checkDBVersion()
# 14 -> 15
class AddDBIncreaseTo15(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Bumping database version to v%s' % self.checkDBVersion())
self.incDBVersion()
return self.checkDBVersion()
@ -521,12 +517,14 @@ class AddDBIncreaseTo15(db.SchemaUpgrade):
# 15 -> 16
class AddIMDbInfo(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Creating IMDb table imdb_info')
self.connection.action(
'CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)')
if not self.hasColumn('tv_shows', 'imdb_id'):
logger.log(u'Adding IMDb column imdb_id to tv_shows')
self.addColumn('tv_shows', 'imdb_id')
self.incDBVersion()
@ -536,9 +534,18 @@ class AddIMDbInfo(db.SchemaUpgrade):
# 16 -> 17
class AddProperNamingSupport(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasColumn('tv_shows', 'imdb_id')\
and self.hasColumn('tv_shows', 'rls_require_words')\
and self.hasColumn('tv_shows', 'rls_ignore_words'):
self.setDBVersion(5816)
return self.checkDBVersion()
if not self.hasColumn('tv_episodes', 'is_proper'):
logger.log(u'Adding column is_proper to tv_episodes')
self.addColumn('tv_episodes', 'is_proper')
self.addColumn('tv_episodes', 'is_proper')
self.incDBVersion()
return self.checkDBVersion()
@ -546,9 +553,19 @@ class AddProperNamingSupport(db.SchemaUpgrade):
# 17 -> 18
class AddEmailSubscriptionTable(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasColumn('tv_episodes', 'is_proper')\
and self.hasColumn('tv_shows', 'rls_require_words')\
and self.hasColumn('tv_shows', 'rls_ignore_words')\
and self.hasColumn('tv_shows', 'skip_notices'):
self.setDBVersion(5817)
return self.checkDBVersion()
if not self.hasColumn('tv_shows', 'notify_list'):
logger.log(u'Adding column notify_list to tv_shows')
self.addColumn('tv_shows', 'notify_list', 'TEXT', None)
self.addColumn('tv_shows', 'notify_list', 'TEXT', None)
self.incDBVersion()
return self.checkDBVersion()
@ -556,10 +573,18 @@ class AddEmailSubscriptionTable(db.SchemaUpgrade):
# 18 -> 19
class AddProperSearch(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasColumn('tv_shows', 'notify_list')\
and self.hasColumn('tv_shows', 'rls_require_words')\
and self.hasColumn('tv_shows', 'rls_ignore_words')\
and self.hasColumn('tv_shows', 'skip_notices')\
and self.hasColumn('history', 'source'):
self.setDBVersion(5818)
return self.checkDBVersion()
logger.log(u'Adding column last_proper_search to info')
if not self.hasColumn('info', 'last_proper_search'):
logger.log(u'Adding column last_proper_search to info')
self.addColumn('info', 'last_proper_search', default=1)
self.incDBVersion()
@ -569,10 +594,10 @@ class AddProperSearch(db.SchemaUpgrade):
# 19 -> 20
class AddDvdOrderOption(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column dvdorder to tvshows')
if not self.hasColumn('tv_shows', 'dvdorder'):
logger.log(u'Adding column dvdorder to tv_shows')
self.addColumn('tv_shows', 'dvdorder', 'NUMERIC', '0')
self.incDBVersion()
@ -582,13 +607,15 @@ class AddDvdOrderOption(db.SchemaUpgrade):
# 20 -> 21
class AddSubtitlesSupport(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasColumn('tv_shows', 'subtitles'):
logger.log(u'Adding subtitles to tv_shows and tv_episodes')
self.addColumn('tv_shows', 'subtitles')
self.addColumn('tv_episodes', 'subtitles', 'TEXT', '')
self.addColumn('tv_episodes', 'subtitles_searchcount')
self.addColumn('tv_episodes', 'subtitles_lastsearch', 'TIMESTAMP', str(datetime.datetime.min))
self.incDBVersion()
return self.checkDBVersion()
@ -596,7 +623,7 @@ class AddSubtitlesSupport(db.SchemaUpgrade):
# 21 -> 22
class ConvertTVShowsToIndexerScheme(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Converting TV Shows table to Indexer Scheme...')
@ -623,7 +650,7 @@ class ConvertTVShowsToIndexerScheme(db.SchemaUpgrade):
# 22 -> 23
class ConvertTVEpisodesToIndexerScheme(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Converting TV Episodes table to Indexer Scheme...')
@ -653,7 +680,7 @@ class ConvertTVEpisodesToIndexerScheme(db.SchemaUpgrade):
# 23 -> 24
class ConvertIMDBInfoToIndexerScheme(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Converting IMDB Info table to Indexer Scheme...')
@ -675,7 +702,7 @@ class ConvertIMDBInfoToIndexerScheme(db.SchemaUpgrade):
# 24 -> 25
class ConvertInfoToIndexerScheme(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Converting Info table to Indexer Scheme...')
@ -697,10 +724,10 @@ class ConvertInfoToIndexerScheme(db.SchemaUpgrade):
# 25 -> 26
class AddArchiveFirstMatchOption(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column archive_firstmatch to tvshows')
if not self.hasColumn('tv_shows', 'archive_firstmatch'):
logger.log(u'Adding column archive_firstmatch to tv_shows')
self.addColumn('tv_shows', 'archive_firstmatch', 'NUMERIC', '0')
self.incDBVersion()
@ -711,13 +738,14 @@ class AddArchiveFirstMatchOption(db.SchemaUpgrade):
class AddSceneNumbering(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if self.hasTable('scene_numbering'):
self.connection.action('DROP TABLE scene_numbering')
logger.log(u'Upgrading table scene_numbering ...', logger.MESSAGE)
self.connection.action(
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY (indexer_id, season, episode, scene_season, scene_episode))')
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY (indexer_id,season,episode))')
self.incDBVersion()
return self.checkDBVersion()
@ -726,7 +754,7 @@ class AddSceneNumbering(db.SchemaUpgrade):
# 27 -> 28
class ConvertIndexerToInteger(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
cl = []
logger.log(u'Converting Indexer to Integer ...', logger.MESSAGE)
@ -748,14 +776,18 @@ class AddRequireAndIgnoreWords(db.SchemaUpgrade):
# Adding column rls_require_words and rls_ignore_words to tv_shows
def execute(self):
backup_database(self.checkDBVersion())
if self.hasColumn('tv_shows', 'rls_require_words') and self.hasColumn('tv_shows', 'rls_ignore_words'):
self.incDBVersion()
return self.checkDBVersion()
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column rls_require_words to tvshows')
if not self.hasColumn('tv_shows', 'rls_require_words'):
logger.log(u'Adding column rls_require_words to tv_shows')
self.addColumn('tv_shows', 'rls_require_words', 'TEXT', '')
logger.log(u'Adding column rls_ignore_words to tvshows')
if not self.hasColumn('tv_shows', 'rls_ignore_words'):
logger.log(u'Adding column rls_ignore_words to tv_shows')
self.addColumn('tv_shows', 'rls_ignore_words', 'TEXT', '')
self.incDBVersion()
@ -765,10 +797,10 @@ class AddRequireAndIgnoreWords(db.SchemaUpgrade):
# 29 -> 30
class AddSportsOption(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column sports to tvshows')
if not self.hasColumn('tv_shows', 'sports'):
logger.log(u'Adding column sports to tv_shows')
self.addColumn('tv_shows', 'sports', 'NUMERIC', '0')
if self.hasColumn('tv_shows', 'air_by_date') and self.hasColumn('tv_shows', 'sports'):
@ -790,20 +822,20 @@ class AddSportsOption(db.SchemaUpgrade):
# 30 -> 31
class AddSceneNumberingToTvEpisodes(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column scene_season and scene_episode to tvepisodes')
logger.log(u'Adding columns scene_season and scene_episode to tvepisodes')
self.addColumn('tv_episodes', 'scene_season', 'NUMERIC', 'NULL')
self.addColumn('tv_episodes', 'scene_episode', 'NUMERIC', 'NULL')
self.incDBVersion()
return self.incDBVersion()
return self.checkDBVersion()
# 31 -> 32
class AddAnimeTVShow(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column anime to tv_episodes')
self.addColumn('tv_shows', 'anime', 'NUMERIC', '0')
@ -815,7 +847,7 @@ class AddAnimeTVShow(db.SchemaUpgrade):
# 32 -> 33
class AddAbsoluteNumbering(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column absolute_number to tv_episodes')
self.addColumn('tv_episodes', 'absolute_number', 'NUMERIC', '0')
@ -827,9 +859,9 @@ class AddAbsoluteNumbering(db.SchemaUpgrade):
# 33 -> 34
class AddSceneAbsoluteNumbering(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column absolute_number and scene_absolute_number to scene_numbering')
logger.log(u'Adding columns absolute_number and scene_absolute_number to scene_numbering')
self.addColumn('scene_numbering', 'absolute_number', 'NUMERIC', '0')
self.addColumn('scene_numbering', 'scene_absolute_number', 'NUMERIC', '0')
@ -840,11 +872,12 @@ class AddSceneAbsoluteNumbering(db.SchemaUpgrade):
# 34 -> 35
class AddAnimeBlacklistWhitelist(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
cl = []
cl.append(['CREATE TABLE blacklist (show_id INTEGER, range TEXT, keyword TEXT)'])
cl.append(['CREATE TABLE whitelist (show_id INTEGER, range TEXT, keyword TEXT)'])
logger.log(u'Creating table blacklist whitelist')
self.connection.mass_action(cl)
self.incDBVersion()
@ -854,7 +887,7 @@ class AddAnimeBlacklistWhitelist(db.SchemaUpgrade):
# 35 -> 36
class AddSceneAbsoluteNumbering2(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column scene_absolute_number to tv_episodes')
self.addColumn('tv_episodes', 'scene_absolute_number', 'NUMERIC', '0')
@ -866,7 +899,7 @@ class AddSceneAbsoluteNumbering2(db.SchemaUpgrade):
# 36 -> 37
class AddXemRefresh(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Creating table xem_refresh')
self.connection.action(
@ -879,7 +912,7 @@ class AddXemRefresh(db.SchemaUpgrade):
# 37 -> 38
class AddSceneToTvShows(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column scene to tv_shows')
self.addColumn('tv_shows', 'scene', 'NUMERIC', '0')
@ -891,7 +924,7 @@ class AddSceneToTvShows(db.SchemaUpgrade):
# 38 -> 39
class AddIndexerMapping(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
if self.hasTable('indexer_mapping'):
self.connection.action('DROP TABLE indexer_mapping')
@ -907,11 +940,13 @@ class AddIndexerMapping(db.SchemaUpgrade):
# 39 -> 40
class AddVersionToTvEpisodes(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column version to tv_episodes and history')
self.addColumn('tv_episodes', 'version', 'NUMERIC', '-1')
logger.log(u'Adding columns release_group and version to tv_episodes')
self.addColumn('tv_episodes', 'release_group', 'TEXT', '')
self.addColumn('tv_episodes', 'version', 'NUMERIC', '-1')
logger.log(u'Adding column version to history')
self.addColumn('history', 'version', 'NUMERIC', '-1')
self.incDBVersion()
@ -921,7 +956,8 @@ class AddVersionToTvEpisodes(db.SchemaUpgrade):
# 40 -> 10000
class BumpDatabaseVersion(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Bumping database version')
self.setDBVersion(10000)
@ -931,7 +967,7 @@ class BumpDatabaseVersion(db.SchemaUpgrade):
# 41 -> 10001
class Migrate41(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Bumping database version')
@ -939,34 +975,62 @@ class Migrate41(db.SchemaUpgrade):
return self.checkDBVersion()
# 5816 - 5818 -> 15
class MigrateUpstream(db.SchemaUpgrade):
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Migrate SickBeard DB v%s into v15' % str(self.checkDBVersion()).replace('58', ''))
self.setDBVersion(15)
return self.checkDBVersion()
# 10000 -> 20000
class SickGearDatabaseVersion(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log('Bumping database version to new SickGear standards')
logger.log(u'Bumping database version to new SickGear standards')
self.setDBVersion(20000)
return self.checkDBVersion()
# 20000 -> 20001
class DBIncreaseTo20001(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
logger.log('Bumping database version to force a backup before new database code')
self.setDBVersion(20001)
return self.checkDBVersion()
# 10001 -> 10000
class RemoveDefaultEpStatusFromTvShows(db.SchemaUpgrade):
def execute(self):
backup_database(self.checkDBVersion())
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Dropping column default_ep_status from tv_shows')
self.dropColumn('tv_shows', 'default_ep_status')
self.setDBVersion(10000)
return self.checkDBVersion()
# 20000 -> 20001
class DBIncreaseTo20001(db.SchemaUpgrade):
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Bumping database version to force a backup before new database code')
self.connection.action('VACUUM')
logger.log(u'Performed a vacuum on the database', logger.DEBUG)
self.setDBVersion(20001)
return self.checkDBVersion()
# 20001 -> 20002
class AddTvShowOverview(db.SchemaUpgrade):
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
logger.log(u'Adding column overview to tv_shows')
self.addColumn('tv_shows', 'overview', 'TEXT', '')
self.setDBVersion(20002)
return self.checkDBVersion()

View file

@ -25,15 +25,15 @@ import time
import threading
import sickbeard
from sickbeard import encodingKludge as ek
from sickbeard import logger
from sickbeard.exceptions import ex
db_lock = threading.Lock()
def dbFilename(filename="sickbeard.db", suffix=None):
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
@ -42,16 +42,16 @@ def dbFilename(filename="sickbeard.db", suffix=None):
@return: the correct location of the database file.
"""
if suffix:
filename = "%s.%s" % (filename, suffix)
filename = '%s.%s' % (filename, suffix)
return ek.ek(os.path.join, sickbeard.DATA_DIR, filename)
class DBConnection(object):
def __init__(self, filename="sickbeard.db", suffix=None, row_type=None):
def __init__(self, filename='sickbeard.db', suffix=None, row_type=None):
self.filename = filename
self.connection = sqlite3.connect(dbFilename(filename), 20)
if row_type == "dict":
if row_type == 'dict':
self.connection.row_factory = self._dict_factory
else:
self.connection.row_factory = sqlite3.Row
@ -62,12 +62,12 @@ class DBConnection(object):
try:
if self.hasTable('db_version'):
result = self.select("SELECT db_version FROM db_version")
result = self.select('SELECT db_version FROM db_version')
except:
return 0
if result:
return int(result[0]["db_version"])
return int(result[0]['db_version'])
else:
return 0
@ -90,27 +90,26 @@ class DBConnection(object):
sqlResult.append(self.connection.execute(qu[0]).fetchall())
elif len(qu) > 1:
if logTransaction:
logger.log(qu[0] + " with args " + str(qu[1]), logger.DB)
logger.log(qu[0] + ' with args ' + str(qu[1]), logger.DB)
sqlResult.append(self.connection.execute(qu[0], qu[1]).fetchall())
self.connection.commit()
logger.log(u"Transaction with " + str(len(querylist)) + u" query's executed", logger.DEBUG)
logger.log(u'Transaction with ' + str(len(querylist)) + u' queries executed', logger.DEBUG)
return sqlResult
except sqlite3.OperationalError, e:
sqlResult = []
if self.connection:
self.connection.rollback()
if "unable to open database file" in e.args[0] or "database is locked" in e.args[0]:
logger.log(u"DB error: " + ex(e), logger.WARNING)
if 'unable to open database file' in e.args[0] or 'database is locked' in e.args[0]:
logger.log(u'DB error: ' + ex(e), logger.WARNING)
attempt += 1
time.sleep(1)
else:
logger.log(u"DB error: " + ex(e), logger.ERROR)
logger.log(u'DB error: ' + ex(e), logger.ERROR)
raise
except sqlite3.DatabaseError, e:
sqlResult = []
if self.connection:
self.connection.rollback()
logger.log(u"Fatal error executing query: " + ex(e), logger.ERROR)
logger.log(u'Fatal error executing query: ' + ex(e), logger.ERROR)
raise
return sqlResult
@ -128,24 +127,24 @@ class DBConnection(object):
while attempt < 5:
try:
if args is None:
logger.log(self.filename + ": " + query, logger.DB)
logger.log(self.filename + ': ' + query, logger.DB)
sqlResult = self.connection.execute(query)
else:
logger.log(self.filename + ": " + query + " with args " + str(args), logger.DB)
logger.log(self.filename + ': ' + query + ' with args ' + str(args), logger.DB)
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.args[0] or "database is locked" in e.args[0]:
logger.log(u"DB error: " + ex(e), logger.WARNING)
if 'unable to open database file' in e.args[0] or 'database is locked' in e.args[0]:
logger.log(u'DB error: ' + ex(e), logger.WARNING)
attempt += 1
time.sleep(1)
else:
logger.log(u"DB error: " + ex(e), logger.ERROR)
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)
logger.log(u'Fatal error executing query: ' + ex(e), logger.ERROR)
raise
return sqlResult
@ -159,41 +158,41 @@ class DBConnection(object):
return sqlResults
def upsert(self, tableName, valueDict, keyDict):
changesBefore = self.connection.total_changes
genParams = lambda myDict: [x + " = ?" for x in myDict.keys()]
genParams = lambda myDict: [x + ' = ?' for x in myDict.keys()]
query = "UPDATE [" + tableName + "] SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(
genParams(keyDict))
query = 'UPDATE [%s] SET %s WHERE %s' % (
tableName, ', '.join(genParams(valueDict)), ' 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())) + ")"
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
sqlResult = self.select("PRAGMA table_info([%s])" % tableName)
sqlResult = self.select('PRAGMA table_info([%s])' % tableName)
columns = {}
for column in sqlResult:
columns[column['name']] = {'type': column['type']}
return columns
# http://stackoverflow.com/questions/3300464/how-can-i-get-dict-from-sqlite-query
def _dict_factory(self, cursor, row):
@staticmethod
def _dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
def hasTable(self, tableName):
return len(self.select("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, ))) > 0
return len(self.select('SELECT 1 FROM sqlite_master WHERE name = ?;', (tableName, ))) > 0
def hasColumn(self, tableName, column):
return column in self.tableInfo(tableName)
@ -205,17 +204,17 @@ class DBConnection(object):
return True
return False
def addColumn(self, table, column, type="NUMERIC", default=0):
self.action("ALTER TABLE [%s] ADD %s %s" % (table, column, type))
self.action("UPDATE [%s] SET %s = ?" % (table, column), (default,))
def addColumn(self, table, column, type='NUMERIC', default=0):
self.action('ALTER TABLE [%s] ADD %s %s' % (table, column, type))
self.action('UPDATE [%s] SET %s = ?' % (table, column), (default,))
def close(self):
"""Close database connection"""
if getattr(self, "connection", None) is not None:
if getattr(self, 'connection', None) is not None:
self.connection.close()
self.connection = None
def sanityCheckDatabase(connection, sanity_check):
sanity_check(connection).check()
@ -228,23 +227,19 @@ class DBSanityCheck(object):
pass
# ===============
# = Upgrade API =
# ===============
def upgradeDatabase(connection, schema):
logger.log(u"Checking database structure...", logger.MESSAGE)
logger.log(u'Checking database structure...', logger.MESSAGE)
_processUpgrade(connection, schema)
def prettyName(class_name):
return ' '.join([x.group() for x in re.finditer("([A-Z])([a-z0-9]+)", class_name)])
return ' '.join([x.group() for x in re.finditer('([A-Z])([a-z0-9]+)', class_name)])
def restoreDatabase(version):
logger.log(u"Restoring database before trying upgrade again")
if not sickbeard.helpers.restoreVersionedFile(dbFilename(suffix='v' + str(version)), version):
logger.log_error_and_exit(u"Database restore failed, abort upgrading database")
def restoreDatabase(filename, version):
logger.log(u'Restoring database before trying upgrade again')
if not sickbeard.helpers.restoreVersionedFile(dbFilename(filename=filename, suffix='v%s' % version), version):
logger.log_error_and_exit(u'Database restore failed, abort upgrading database')
return False
else:
return True
@ -252,9 +247,9 @@ def restoreDatabase(version):
def _processUpgrade(connection, upgradeClass):
instance = upgradeClass(connection)
logger.log(u"Checking " + prettyName(upgradeClass.__name__) + " database upgrade", logger.DEBUG)
logger.log(u'Checking %s database upgrade' % prettyName(upgradeClass.__name__), logger.DEBUG)
if not instance.test():
logger.log(u"Database upgrade required: " + prettyName(upgradeClass.__name__), logger.MESSAGE)
logger.log(u'Database upgrade required: %s' % prettyName(upgradeClass.__name__), logger.MESSAGE)
try:
instance.execute()
except sqlite3.DatabaseError, e:
@ -262,25 +257,21 @@ def _processUpgrade(connection, upgradeClass):
try:
instance.execute()
except:
restored = False
result = connection.select("SELECT db_version FROM db_version")
result = connection.select('SELECT db_version FROM db_version')
if result:
version = int(result[0]["db_version"])
version = int(result[0]['db_version'])
# close db before attempting restore
connection.close()
if restoreDatabase(version):
# initialize the main SB database
upgradeDatabase(DBConnection(), sickbeard.mainDB.InitialSchema)
restored = True
if restoreDatabase(connection.filename, version):
logger.log_error_and_exit(u'Successfully restored database version: %s' % version)
else:
logger.log_error_and_exit(u'Failed to restore database version: %s' % version)
if not restored:
print "Error in " + str(upgradeClass.__name__) + ": " + ex(e)
raise
logger.log(upgradeClass.__name__ + " upgrade completed", logger.DEBUG)
logger.log('%s upgrade completed' % upgradeClass.__name__, logger.DEBUG)
else:
logger.log(upgradeClass.__name__ + " upgrade not required", logger.DEBUG)
logger.log('%s upgrade not required' % upgradeClass.__name__, logger.DEBUG)
for upgradeSubClass in upgradeClass.__subclasses__():
_processUpgrade(connection, upgradeSubClass)
@ -292,14 +283,14 @@ class SchemaUpgrade(object):
self.connection = connection
def hasTable(self, tableName):
return len(self.connection.select("SELECT 1 FROM sqlite_master WHERE name = ?;", (tableName, ))) > 0
return len(self.connection.select('SELECT 1 FROM sqlite_master WHERE name = ?;', (tableName, ))) > 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 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 dropColumn(self, table, column):
# get old table columns and store the ones we want to keep
@ -315,9 +306,7 @@ class SchemaUpgrade(object):
keptColumnsNames.append(column['name'])
cl = []
cl.append(column['name'])
cl.append(column['type'])
cl = [column['name'], column['type']]
'''
To be implemented if ever required
@ -350,7 +339,7 @@ class SchemaUpgrade(object):
self.connection.action('INSERT INTO [%s_new] SELECT %s FROM [%s]' % (table, keptColumnsNames, table))
# copy the old indexes from the old table
result = self.connection.select('SELECT sql FROM sqlite_master WHERE tbl_name=? and type="index"', [table])
result = self.connection.select("SELECT sql FROM sqlite_master WHERE tbl_name=? and type='index'", [table])
# remove the old table and rename the new table to take it's place
self.connection.action('DROP TABLE [%s]' % table)
@ -362,25 +351,31 @@ class SchemaUpgrade(object):
self.connection.action(index['sql'])
# vacuum the db as we will have a lot of space to reclaim after dropping tables
self.connection.action("VACUUM")
self.connection.action('VACUUM')
def checkDBVersion(self):
return self.connection.checkDBVersion()
def incDBVersion(self):
new_version = self.checkDBVersion() + 1
self.connection.action("UPDATE db_version SET db_version = ?", [new_version])
self.connection.action('UPDATE db_version SET db_version = ?', [new_version])
return new_version
def setDBVersion(self, new_version):
self.connection.action("UPDATE db_version SET db_version = ?", [new_version])
self.connection.action('UPDATE db_version SET db_version = ?', [new_version])
return new_version
def listTables(self):
tables = []
sql_result = self.connection.select('SELECT name FROM sqlite_master where type = "table"')
for table in sql_result:
tables.append(table[0])
return tables
def MigrationCode(myDB):
schema = {
0: sickbeard.mainDB.InitialSchema, # 0->20000
0: sickbeard.mainDB.InitialSchema,
9: sickbeard.mainDB.AddSizeAndSceneNameFields,
10: sickbeard.mainDB.RenameSeasonFolders,
11: sickbeard.mainDB.Add1080pAndRawHDQualities,
@ -415,17 +410,22 @@ def MigrationCode(myDB):
40: sickbeard.mainDB.BumpDatabaseVersion,
41: sickbeard.mainDB.Migrate41,
42: sickbeard.mainDB.Migrate41,
5816: sickbeard.mainDB.MigrateUpstream,
5817: sickbeard.mainDB.MigrateUpstream,
5818: sickbeard.mainDB.MigrateUpstream,
10000: sickbeard.mainDB.SickGearDatabaseVersion,
10001: sickbeard.mainDB.RemoveDefaultEpStatusFromTvShows,
20000: sickbeard.mainDB.DBIncreaseTo20001,
#20001: sickbeard.mainDB.AddCoolSickGearFeature2,
#20002: sickbeard.mainDB.AddCoolSickGearFeature3,
20001: sickbeard.mainDB.AddTvShowOverview,
# 20002: sickbeard.mainDB.AddCoolSickGearFeature3,
}
db_version = myDB.checkDBVersion()
logger.log(u'Detected database version: v' + str(db_version), logger.DEBUG)
logger.log(u'Detected database version: v%s' % db_version, logger.DEBUG)
if not (db_version in schema):
if db_version == sickbeard.mainDB.MAX_DB_VERSION:
@ -443,10 +443,17 @@ def MigrationCode(myDB):
db_version = update.execute()
except Exception, e:
myDB.close()
logger.log(u'Failed to update database with error: ' + ex(e) + ' attempting recovery...', logger.ERROR)
logger.log(u'Failed to update database with error: %s attempting recovery...' % ex(e), logger.ERROR)
if restoreDatabase(db_version):
if restoreDatabase(myDB.filename, db_version):
# initialize the main SB database
logger.log_error_and_exit(u'Successfully restored database version:' + str(db_version))
logger.log_error_and_exit(u'Successfully restored database version: %s' % db_version)
else:
logger.log_error_and_exit(u'Failed to restore database version:' + str(db_version))
logger.log_error_and_exit(u'Failed to restore database version: %s' % db_version)
def backup_database(filename, version):
logger.log(u'Backing up database before upgrade')
if not sickbeard.helpers.backupVersionedFile(dbFilename(filename), version):
logger.log_error_and_exit(u'Database backup failed, abort upgrading database')
else:
logger.log(u'Proceeding with upgrade')

View file

@ -89,7 +89,8 @@ class GenericQueue(object):
# launch the queue item in a thread
self.currentItem = self.queue.pop(0)
self.currentItem.name = self.queue_name + '-' + self.currentItem.name
if not self.queue_name == 'SEARCHQUEUE':
self.currentItem.name = self.queue_name + '-' + self.currentItem.name
self.currentItem.start()
class QueueItem(threading.Thread):

View file

@ -98,13 +98,14 @@ class GitHub(object):
access_API = self._access_API(
['repos', self.github_repo_user, self.github_repo, 'pulls'],
params={'per_page': 100})
pull = []
pulls = []
for x in access_API:
try:
pull.append(PullRequest(x['head']['ref'], x['number']))
pull = PullRequest(x['head']['ref'], x['number'])
pulls.append((repr(pull), pull.fetch_name()))
except:
continue
return pull
return pulls
class PullRequest(object):
def __init__(self, ref, number):

View file

@ -65,8 +65,6 @@ from sickbeard import clients
from lib.cachecontrol import CacheControl, caches
from itertools import izip, cycle
urllib._urlopener = classes.SickBeardURLopener()
def indentXML(elem, level=0):
'''
@ -173,15 +171,6 @@ def isRarFile(filename):
return False
def isBeingWritten(filepath):
# Return True if file was modified within 60 seconds. it might still be being written to.
ctime = max(ek.ek(os.path.getctime, filepath), ek.ek(os.path.getmtime, filepath))
if ctime > time.time() - 60:
return True
return False
def sanitizeFileName(name):
'''
>>> sanitizeFileName('a/b/c')
@ -232,43 +221,6 @@ def makeDir(path):
return True
def searchDBForShow(regShowName, log=False):
showNames = [re.sub('[. -]', ' ', regShowName)]
yearRegex = "([^()]+?)\s*(\()?(\d{4})(?(2)\))$"
myDB = db.DBConnection()
for showName in showNames:
sqlResults = myDB.select("SELECT * FROM tv_shows WHERE show_name LIKE ?",
[showName])
if len(sqlResults) == 1:
return int(sqlResults[0]["indexer_id"])
else:
# if we didn't get exactly one result then try again with the year stripped off if possible
match = re.match(yearRegex, showName)
if match and match.group(1):
if log:
logger.log(u"Unable to match original name but trying to manually strip and specify show year",
logger.DEBUG)
sqlResults = myDB.select(
"SELECT * FROM tv_shows WHERE (show_name LIKE ?) AND startyear = ?",
[match.group(1) + '%', match.group(3)])
if len(sqlResults) == 0:
if log:
logger.log(u"Unable to match a record in the DB for " + showName, logger.DEBUG)
continue
elif len(sqlResults) > 1:
if log:
logger.log(u"Multiple results for " + showName + " in the DB, unable to match show name",
logger.DEBUG)
continue
else:
return int(sqlResults[0]["indexer_id"])
def searchIndexerForShowID(regShowName, indexer=None, indexer_id=None, ui=None):
showNames = [re.sub('[. -]', ' ', regShowName)]
@ -613,17 +565,6 @@ def fixSetGroupID(childPath):
childPath, parentGID), logger.ERROR)
def is_anime_in_show_list():
for show in sickbeard.showList:
if show.is_anime:
return True
return False
def update_anime_support():
sickbeard.ANIMESUPPORT = is_anime_in_show_list()
def get_absolute_number_from_season_and_episode(show, season, episode):
absolute_number = None
@ -735,24 +676,6 @@ if __name__ == '__main__':
doctest.testmod()
def parse_json(data):
"""
Parse json data into a python object
data: data string containing json
Returns: parsed data as json or None
"""
try:
parsedJSON = json.loads(data)
except ValueError, e:
logger.log(u"Error trying to decode json data. Error: " + ex(e), logger.DEBUG)
return None
return parsedJSON
def parse_xml(data, del_xmlns=False):
"""
Parse data into an xml elementtree.ElementTree
@ -775,32 +698,6 @@ def parse_xml(data, del_xmlns=False):
return parsedXML
def get_xml_text(element, mini_dom=False):
"""
Get all text inside a xml element
element: A xml element either created with elementtree.ElementTree or xml.dom.minidom
mini_dom: Default False use elementtree, True use minidom
Returns: text
"""
text = ""
if mini_dom:
node = element
for child in node.childNodes:
if child.nodeType in (Node.CDATA_SECTION_NODE, Node.TEXT_NODE):
text += child.data
else:
if element is not None:
for child in [element] + element.findall('.//*'):
if child.text:
text += child.text
return text.strip()
def backupVersionedFile(old_file, version):
numTries = 0
@ -961,6 +858,17 @@ def anon_url(*url):
return '' if None in url else '%s%s' % (sickbeard.ANON_REDIRECT, ''.join(str(s) for s in url))
def starify(text, verify=False):
"""
Return text input string with either its latter half or its centre area (if 12 chars or more)
replaced with asterisks. Useful for securely presenting api keys to a ui.
If verify is true, return true if text is a star block created text else return false.
"""
return ((('%s%s' % (text[:len(text) / 2], '*' * (len(text) / 2))),
('%s%s%s' % (text[:4], '*' * (len(text) - 8), text[-4:])))[12 <= len(text)],
set('*') == set((text[len(text) / 2:], text[4:-4])[12 <= len(text)]))[verify]
"""
Encryption
==========
@ -978,6 +886,7 @@ To add a new encryption_version:
# Key Generators
unique_key1 = hex(uuid.getnode() ** 2) # Used in encryption v1
# Encryption Functions
def encrypt(data, encryption_version=0, decrypt=False):
# Version 1: Simple XOR encryption (this is not very secure, but works)
@ -1000,23 +909,8 @@ def full_sanitizeSceneName(name):
return re.sub('[. -]', ' ', sanitizeSceneName(name)).lower().lstrip()
def _check_against_names(nameInQuestion, show, season=-1):
showNames = []
if season in [-1, 1]:
showNames = [show.name]
showNames.extend(sickbeard.scene_exceptions.get_scene_exceptions(show.indexerid, season=season))
for showName in showNames:
nameFromList = full_sanitizeSceneName(showName)
if nameFromList == nameInQuestion:
return True
return False
def get_show(name, tryIndexers=False):
if not sickbeard.showList:
if not sickbeard.showList or None is name:
return
showObj = None
@ -1112,51 +1006,6 @@ def set_up_anidb_connection():
return sickbeard.ADBA_CONNECTION.authed()
def makeZip(fileList, archive):
"""
'fileList' is a list of file names - full path each name
'archive' is the file name for the archive with a full path
"""
try:
a = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
for f in fileList:
a.write(f)
a.close()
return True
except Exception as e:
logger.log(u"Zip creation error: " + str(e), logger.ERROR)
return False
def extractZip(archive, targetDir):
"""
'fileList' is a list of file names - full path each name
'archive' is the file name for the archive with a full path
"""
try:
if not os.path.exists(targetDir):
os.mkdir(targetDir)
zip_file = zipfile.ZipFile(archive, 'r')
for member in zip_file.namelist():
filename = os.path.basename(member)
# skip directories
if not filename:
continue
# copy file (taken from zipfile's extract)
source = zip_file.open(member)
target = file(os.path.join(targetDir, filename), "wb")
shutil.copyfileobj(source, target)
source.close()
target.close()
zip_file.close()
return True
except Exception as e:
logger.log(u"Zip extraction error: " + str(e), logger.ERROR)
return False
def mapIndexersToShow(showObj):
mapped = {}
@ -1360,6 +1209,9 @@ def getURL(url, post_data=None, params=None, headers=None, timeout=30, session=N
except requests.exceptions.Timeout, e:
logger.log(u"Connection timed out " + str(e.message) + " while loading URL " + url, logger.WARNING)
return
except requests.exceptions.ReadTimeout, e:
logger.log(u'Read timed out ' + str(e.message) + ' while loading URL ' + url, logger.WARNING)
return
except Exception:
logger.log(u"Unknown exception while loading URL " + url + ": " + traceback.format_exc(), logger.WARNING)
return
@ -1469,6 +1321,7 @@ def clearCache(force=False):
logger.WARNING)
break
def human(size):
"""
format a size in bytes into a 'human' file size, e.g. bytes, KB, MB, GB, TB, PB
@ -1508,9 +1361,15 @@ def get_size(start_path='.'):
def remove_article(text=''):
return re.sub(r'(?i)^(?:(?:A(?!\s+to)n?)|The)\s(\w)', r'\1', text)
def maybe_plural(number=1):
return ('s', '')[1 == number]
def build_dict(seq, key):
return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq))
def client_host(server_host):
'''Extracted from cherrypy libs
Return the host on which a client can connect to the given listener.'''

View file

@ -22,11 +22,14 @@ import time
import os
import sys
import threading
import zipfile
import logging
import glob
import codecs
from logging.handlers import TimedRotatingFileHandler
import sickbeard
from sickbeard import classes
try:
@ -34,13 +37,6 @@ try:
except ImportError:
pass
# 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
@ -59,16 +55,11 @@ class NullHandler(logging.Handler):
pass
class SBRotatingLogHandler(object):
def __init__(self, log_file, num_files, num_bytes):
self.num_files = num_files
self.num_bytes = num_bytes
def __init__(self, log_file):
self.log_file = log_file
self.log_file_path = log_file
self.cur_handler = None
self.writes_since_check = 0
self.console_logging = False
self.log_lock = threading.Lock()
@ -181,8 +172,8 @@ class SBRotatingLogHandler(object):
Configure a file handler to log at file_name and return it.
"""
file_handler = logging.FileHandler(self.log_file_path, encoding='utf-8')
file_handler.setLevel(DB)
file_handler = TimedCompressedRotatingFileHandler(self.log_file_path, when='midnight', backupCount=7, encoding='utf-8')
file_handler.setLevel(reverseNames[sickbeard.FILE_LOGGING_PRESET])
file_handler.setFormatter(DispatchingFormatter(
{'sickbeard': logging.Formatter('%(asctime)s %(levelname)-8s %(message)s', '%Y-%m-%d %H:%M:%S'),
'subliminal': logging.Formatter('%(asctime)s %(levelname)-8s SUBLIMINAL :: %(message)s',
@ -197,79 +188,10 @@ class SBRotatingLogHandler(object):
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_path + ('.' + 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')
sub_logger = logging.getLogger('subliminal')
imdb_logger = logging.getLogger('imdbpy')
tornado_logger = logging.getLogger('tornado')
feedcache_logger = logging.getLogger('feedcache')
# delete the old handler
if self.cur_handler:
self.close_log()
# 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:
if sickbeard.TRASH_ROTATE_LOGS:
new_name = '%s.%s' % (cur_file_name, int(time.time()))
os.rename(cur_file_name, new_name)
send2trash(new_name)
else:
os.remove(cur_file_name)
else:
os.rename(cur_file_name, self._log_file_name(i + 1))
except OSError:
pass
# the new log handler will always be on the un-numbered .log file
new_file_handler = self._config_handler()
self.cur_handler = new_file_handler
sb_logger.addHandler(new_file_handler)
sub_logger.addHandler(new_file_handler)
imdb_logger.addHandler(new_file_handler)
tornado_logger.addHandler(new_file_handler)
feedcache_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_path) and os.path.getsize(self.log_file_path) >= LOG_SIZE:
self._rotate_logs()
self.writes_since_check = 0
else:
self.writes_since_check += 1
meThread = threading.currentThread().getName()
message = meThread + u" :: " + toLog
@ -323,7 +245,53 @@ class DispatchingFormatter:
return formatter.format(record)
sb_log_instance = SBRotatingLogHandler('sickbeard.log', NUM_LOGS, LOG_SIZE)
class TimedCompressedRotatingFileHandler(TimedRotatingFileHandler):
"""
Extended version of TimedRotatingFileHandler that compress logs on rollover.
by Angel Freire <cuerty at gmail dot com>
"""
def doRollover(self):
"""
do a rollover; in this case, a date/time stamp is appended to the filename
when the rollover happens. However, you want the file to be named for the
start of the interval, not the current time. If there is a backup count,
then we have to get a list of matching filenames, sort them and remove
the one with the oldest suffix.
This method is a copy of the one in TimedRotatingFileHandler. Since it uses
"""
self.stream.close()
# get the time that this sequence started at and make it a TimeTuple
t = self.rolloverAt - self.interval
timeTuple = time.localtime(t)
file_name = self.baseFilename.rpartition('.')[0]
dfn = '%s_%s.log' % (file_name, time.strftime(self.suffix, timeTuple))
if os.path.exists(dfn):
os.remove(dfn)
os.rename(self.baseFilename, dfn)
if self.backupCount > 0:
# find the oldest log file and delete it
s = glob.glob(file_name + '_*')
if len(s) > self.backupCount:
s.sort()
os.remove(s[0])
#print "%s -> %s" % (self.baseFilename, dfn)
if self.encoding:
self.stream = codecs.open(self.baseFilename, 'w', self.encoding)
else:
self.stream = open(self.baseFilename, 'w')
self.rolloverAt = self.rolloverAt + self.interval
zip_name = dfn.rpartition('.')[0] + '.zip'
if os.path.exists(zip_name):
os.remove(zip_name)
file = zipfile.ZipFile(zip_name, 'w')
file.write(dfn, os.path.basename(dfn), zipfile.ZIP_DEFLATED)
file.close()
os.remove(dfn)
sb_log_instance = SBRotatingLogHandler('sickbeard.log')
def log(toLog, logLevel=MESSAGE):
@ -335,4 +303,9 @@ def log_error_and_exit(error_msg):
def close():
sb_log_instance.close_log()
sb_log_instance.close_log()
def log_set_level():
if sb_log_instance.cur_handler:
sb_log_instance.cur_handler.setLevel(reverseNames[sickbeard.FILE_LOGGING_PRESET])

View file

@ -16,10 +16,18 @@
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
__all__ = ['generic', 'helpers', 'xbmc', 'xbmc_12plus', 'mediabrowser', 'ps3', 'wdtv', 'tivo', 'mede8er']
__all__ = ['generic', 'helpers', 'kodi', 'mede8er', 'mediabrowser', 'ps3', 'tivo', 'wdtv', 'xbmc', 'xbmc_12plus']
import sys
import xbmc, xbmc_12plus, mediabrowser, ps3, wdtv, tivo, mede8er
import kodi
import mede8er
import mediabrowser
import ps3
import tivo
import wdtv
import xbmc
import xbmc_12plus
def available_generators():

376
sickbeard/metadata/kodi.py Normal file
View file

@ -0,0 +1,376 @@
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickGear.
#
# SickGear 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.
#
# SickGear 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 SickGear. If not, see <http://www.gnu.org/licenses/>.
import generic
import datetime
import sickbeard
from sickbeard import logger, exceptions, helpers
from sickbeard.exceptions import ex
import xml.etree.cElementTree as etree
class KODIMetadata(generic.GenericMetadata):
"""
Metadata generation class for Kodi.
The following file structure is used:
show_root/tvshow.nfo (show metadata)
show_root/fanart.jpg (fanart)
show_root/poster.jpg (poster)
show_root/banner.jpg (banner)
show_root/Season ##/filename.ext (*)
show_root/Season ##/filename.nfo (episode metadata)
show_root/Season ##/filename-thumb.jpg (episode thumb)
show_root/season##-poster.jpg (season posters)
show_root/season##-banner.jpg (season banners)
show_root/season-all-poster.jpg (season all poster)
show_root/season-all-banner.jpg (season all banner)
"""
def __init__(self,
show_metadata=False,
episode_metadata=False,
fanart=False,
poster=False,
banner=False,
episode_thumbnails=False,
season_posters=False,
season_banners=False,
season_all_poster=False,
season_all_banner=False):
generic.GenericMetadata.__init__(self,
show_metadata,
episode_metadata,
fanart,
poster,
banner,
episode_thumbnails,
season_posters,
season_banners,
season_all_poster,
season_all_banner)
self.name = 'Kodi'
self.poster_name = 'poster.jpg'
self.season_all_poster_name = 'season-all-poster.jpg'
# web-ui metadata template
self.eg_show_metadata = 'tvshow.nfo'
self.eg_episode_metadata = 'Season##\\<i>filename</i>.nfo'
self.eg_fanart = 'fanart.jpg'
self.eg_poster = 'poster.jpg'
self.eg_banner = 'banner.jpg'
self.eg_episode_thumbnails = 'Season##\\<i>filename</i>-thumb.jpg'
self.eg_season_posters = 'season##-poster.jpg'
self.eg_season_banners = 'season##-banner.jpg'
self.eg_season_all_poster = 'season-all-poster.jpg'
self.eg_season_all_banner = 'season-all-banner.jpg'
def _show_data(self, show_obj):
"""
Creates an elementTree XML structure for a Kodi-style tvshow.nfo and
returns the resulting data object.
show_obj: a TVShow instance to create the NFO for
"""
show_ID = show_obj.indexerid
indexer_lang = show_obj.lang
lINDEXER_API_PARMS = sickbeard.indexerApi(show_obj.indexer).api_params.copy()
lINDEXER_API_PARMS['actors'] = True
if indexer_lang and not indexer_lang == 'en':
lINDEXER_API_PARMS['language'] = indexer_lang
if show_obj.dvdorder != 0:
lINDEXER_API_PARMS['dvdorder'] = True
t = sickbeard.indexerApi(show_obj.indexer).indexer(**lINDEXER_API_PARMS)
tv_node = etree.Element('tvshow')
try:
myShow = t[int(show_ID)]
except sickbeard.indexer_shownotfound:
logger.log(u'Unable to find show with id ' + str(show_ID) + ' on ' + sickbeard.indexerApi(
show_obj.indexer).name + ', skipping it', logger.ERROR)
raise
except sickbeard.indexer_error:
logger.log(
u'' + sickbeard.indexerApi(show_obj.indexer).name + ' is down, can\'t use its data to add this show',
logger.ERROR)
raise
# check for title and id
if getattr(myShow, 'seriesname', None) is None or getattr(myShow, 'id', None) is None:
logger.log(u'Incomplete info for show with id ' + str(show_ID) + ' on ' + sickbeard.indexerApi(
show_obj.indexer).name + ', skipping it', logger.ERROR)
return False
title = etree.SubElement(tv_node, 'title')
if getattr(myShow, 'seriesname', None) is not None:
title.text = myShow['seriesname']
rating = etree.SubElement(tv_node, 'rating')
if getattr(myShow, 'rating', None) is not None:
rating.text = myShow['rating']
year = etree.SubElement(tv_node, 'year')
if getattr(myShow, 'firstaired', None) is not None:
try:
year_text = str(datetime.datetime.strptime(myShow['firstaired'], '%Y-%m-%d').year)
if year_text:
year.text = year_text
except:
pass
plot = etree.SubElement(tv_node, 'plot')
if getattr(myShow, 'overview', None) is not None:
plot.text = myShow['overview']
episodeguide = etree.SubElement(tv_node, 'episodeguide')
episodeguideurl = etree.SubElement(episodeguide, 'url')
episodeguideurl2 = etree.SubElement(tv_node, 'episodeguideurl')
if getattr(myShow, 'id', None) is not None:
showurl = sickbeard.indexerApi(show_obj.indexer).config['base_url'] + str(myShow['id']) + '/all/en.zip'
episodeguideurl.text = showurl
episodeguideurl2.text = showurl
mpaa = etree.SubElement(tv_node, 'mpaa')
if getattr(myShow, 'contentrating', None) is not None:
mpaa.text = myShow['contentrating']
indexerid = etree.SubElement(tv_node, 'id')
if getattr(myShow, 'id', None) is not None:
indexerid.text = str(myShow['id'])
indexer = etree.SubElement(tv_node, 'indexer')
if show_obj.indexer is not None:
indexer.text = str(show_obj.indexer)
genre = etree.SubElement(tv_node, 'genre')
if getattr(myShow, 'genre', None) is not None:
if isinstance(myShow['genre'], basestring):
genre.text = ' / '.join(x.strip() for x in myShow['genre'].split('|') if x.strip())
premiered = etree.SubElement(tv_node, 'premiered')
if getattr(myShow, 'firstaired', None) is not None:
premiered.text = myShow['firstaired']
studio = etree.SubElement(tv_node, 'studio')
if getattr(myShow, 'network', None) is not None:
studio.text = myShow['network']
if getattr(myShow, '_actors', None) is not None:
for actor in myShow['_actors']:
cur_actor = etree.SubElement(tv_node, 'actor')
cur_actor_name = etree.SubElement(cur_actor, 'name')
cur_actor_name_text = actor['name']
if isinstance(cur_actor_name_text, basestring):
cur_actor_name.text = cur_actor_name_text.strip()
cur_actor_role = etree.SubElement(cur_actor, 'role')
cur_actor_role_text = actor['role']
if cur_actor_role_text != None:
cur_actor_role.text = cur_actor_role_text
cur_actor_thumb = etree.SubElement(cur_actor, 'thumb')
cur_actor_thumb_text = actor['image']
if cur_actor_thumb_text != None:
cur_actor_thumb.text = cur_actor_thumb_text
# Make it purdy
helpers.indentXML(tv_node)
data = etree.ElementTree(tv_node)
return data
def _ep_data(self, ep_obj):
"""
Creates an elementTree XML structure for a Kodi-style episode.nfo and
returns the resulting data object.
show_obj: a TVEpisode instance to create the NFO for
"""
eps_to_write = [ep_obj] + ep_obj.relatedEps
indexer_lang = ep_obj.show.lang
lINDEXER_API_PARMS = sickbeard.indexerApi(ep_obj.show.indexer).api_params.copy()
lINDEXER_API_PARMS['actors'] = True
if indexer_lang and not indexer_lang == 'en':
lINDEXER_API_PARMS['language'] = indexer_lang
if ep_obj.show.dvdorder != 0:
lINDEXER_API_PARMS['dvdorder'] = True
try:
t = sickbeard.indexerApi(ep_obj.show.indexer).indexer(**lINDEXER_API_PARMS)
myShow = t[ep_obj.show.indexerid]
except sickbeard.indexer_shownotfound, e:
raise exceptions.ShowNotFoundException(e.message)
except sickbeard.indexer_error, e:
logger.log(u'Unable to connect to ' + sickbeard.indexerApi(
ep_obj.show.indexer).name + ' while creating meta files - skipping - ' + ex(e), logger.ERROR)
return
if len(eps_to_write) > 1:
rootNode = etree.Element('xbmcmultiepisode')
else:
rootNode = etree.Element('episodedetails')
# write an NFO containing info for all matching episodes
for curEpToWrite in eps_to_write:
try:
myEp = myShow[curEpToWrite.season][curEpToWrite.episode]
except (sickbeard.indexer_episodenotfound, sickbeard.indexer_seasonnotfound):
logger.log(u'Unable to find episode ' + str(curEpToWrite.season) + 'x' + str(
curEpToWrite.episode) + ' on ' + sickbeard.indexerApi(
ep_obj.show.indexer).name + '.. has it been removed? Should I delete from db?')
return None
if getattr(myEp, 'firstaired', None) is None:
myEp['firstaired'] = str(datetime.date.fromordinal(1))
if getattr(myEp, 'episodename', None) is None:
logger.log(u'Not generating nfo because the ep has no title', logger.DEBUG)
return None
logger.log(u'Creating metadata for episode ' + str(ep_obj.season) + 'x' + str(ep_obj.episode), logger.DEBUG)
if len(eps_to_write) > 1:
episode = etree.SubElement(rootNode, 'episodedetails')
else:
episode = rootNode
title = etree.SubElement(episode, 'title')
if curEpToWrite.name != None:
title.text = curEpToWrite.name
showtitle = etree.SubElement(episode, 'showtitle')
if curEpToWrite.show.name != None:
showtitle.text = curEpToWrite.show.name
season = etree.SubElement(episode, 'season')
season.text = str(curEpToWrite.season)
episodenum = etree.SubElement(episode, 'episode')
episodenum.text = str(curEpToWrite.episode)
uniqueid = etree.SubElement(episode, 'uniqueid')
uniqueid.text = str(curEpToWrite.indexerid)
aired = etree.SubElement(episode, 'aired')
if curEpToWrite.airdate != datetime.date.fromordinal(1):
aired.text = str(curEpToWrite.airdate)
else:
aired.text = ''
plot = etree.SubElement(episode, 'plot')
if curEpToWrite.description != None:
plot.text = curEpToWrite.description
runtime = etree.SubElement(episode, 'runtime')
if curEpToWrite.season != 0:
if getattr(myShow, 'runtime', None) is not None:
runtime.text = myShow['runtime']
displayseason = etree.SubElement(episode, 'displayseason')
if getattr(myEp, 'airsbefore_season', None) is not None:
displayseason_text = myEp['airsbefore_season']
if displayseason_text != None:
displayseason.text = displayseason_text
displayepisode = etree.SubElement(episode, 'displayepisode')
if getattr(myEp, 'airsbefore_episode', None) is not None:
displayepisode_text = myEp['airsbefore_episode']
if displayepisode_text != None:
displayepisode.text = displayepisode_text
thumb = etree.SubElement(episode, 'thumb')
thumb_text = getattr(myEp, 'filename', None)
if thumb_text != None:
thumb.text = thumb_text
watched = etree.SubElement(episode, 'watched')
watched.text = 'false'
credits = etree.SubElement(episode, 'credits')
credits_text = getattr(myEp, 'writer', None)
if credits_text != None:
credits.text = credits_text
director = etree.SubElement(episode, 'director')
director_text = getattr(myEp, 'director', None)
if director_text is not None:
director.text = director_text
rating = etree.SubElement(episode, 'rating')
rating_text = getattr(myEp, 'rating', None)
if rating_text != None:
rating.text = rating_text
gueststar_text = getattr(myEp, 'gueststars', None)
if isinstance(gueststar_text, basestring):
for actor in (x.strip() for x in gueststar_text.split('|') if x.strip()):
cur_actor = etree.SubElement(episode, 'actor')
cur_actor_name = etree.SubElement(cur_actor, 'name')
cur_actor_name.text = actor
if getattr(myEp, '_actors', None) is not None:
for actor in myShow['_actors']:
cur_actor = etree.SubElement(episode, 'actor')
cur_actor_name = etree.SubElement(cur_actor, 'name')
cur_actor_name_text = actor['name']
if isinstance(cur_actor_name_text, basestring):
cur_actor_name.text = cur_actor_name_text.strip()
cur_actor_role = etree.SubElement(cur_actor, 'role')
cur_actor_role_text = actor['role']
if cur_actor_role_text != None:
cur_actor_role.text = cur_actor_role_text
cur_actor_thumb = etree.SubElement(cur_actor, 'thumb')
cur_actor_thumb_text = actor['image']
if cur_actor_thumb_text != None:
cur_actor_thumb.text = cur_actor_thumb_text
# Make it purdy
helpers.indentXML(rootNode)
data = etree.ElementTree(rootNode)
return data
# present a standard "interface" from the module
metadata_class = KODIMetadata

View file

@ -84,13 +84,14 @@ def buildNameCache(show=None):
global nameCache
with nameCacheLock:
# clear internal name cache
clearCache()
# update scene exception names
sickbeard.scene_exceptions.retrieve_exceptions()
if not show:
# clear internal name cache
clearCache()
logger.log(u"Building internal name cache for all shows", logger.MESSAGE)
cacheDB = db.DBConnection('cache.db')
@ -114,6 +115,9 @@ def buildNameCache(show=None):
nameCache[name] = int(show.indexerid)
else:
# remove old show cache entries
nameCache = dict((k, v) for k, v in nameCache.items() if v != show.indexerid)
logger.log(u"Building internal name cache for " + show.name, logger.MESSAGE)
for curSeason in [-1] + sickbeard.scene_exceptions.get_scene_seasons(show.indexerid):

View file

@ -270,7 +270,8 @@ anime_regexes = [
# Bleach.s16e03-04.313-314
# Bleach s16e03e04 313-314
'''
^(?P<series_name>.+?)[ ._-]+ # start of string and series name and non optinal separator
^(\[(?P<release_group>.+?)\][ ._-]*)?
(?P<series_name>.+?)[ ._-]+ # start of string and series name and non optinal separator
[sS](?P<season_num>\d+)[. _-]* # S01 and optional separator
[eE](?P<ep_num>\d+) # epipisode E02
(([. _-]*e|-) # linking e/- char
@ -374,4 +375,4 @@ anime_regexes = [
-(?P<release_group>[^- ]+))?)?$ # Group
'''
),
]
]

View file

@ -19,6 +19,7 @@
import sickbeard
import xbmc
import kodi
import plex
import nmj
import nmjv2
@ -43,6 +44,7 @@ from sickbeard.common import *
# home theater / nas
xbmc_notifier = xbmc.XBMCNotifier()
kodi_notifier = kodi.KODINotifier()
plex_notifier = plex.PLEXNotifier()
nmj_notifier = nmj.NMJNotifier()
nmjv2_notifier = nmjv2.NMJv2Notifier()
@ -66,6 +68,7 @@ email_notifier = emailnotify.EmailNotifier()
notifiers = [
libnotify_notifier, # Libnotify notifier goes first because it doesn't involve blocking on network activity.
xbmc_notifier,
kodi_notifier,
plex_notifier,
nmj_notifier,
nmjv2_notifier,

241
sickbeard/notifiers/kodi.py Normal file
View file

@ -0,0 +1,241 @@
# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickGear.
#
# SickGear 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.
#
# SickGear 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 SickGear. If not, see <http://www.gnu.org/licenses/>.
import urllib
import urllib2
import socket
import base64
import time
import sickbeard
from sickbeard import logger
from sickbeard import common
from sickbeard.exceptions import ex
from sickbeard.encodingKludge import fixStupidEncodings
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
try:
import json
except ImportError:
from lib import simplejson as json
class KODINotifier:
sg_logo_url = 'https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/ico/apple-touch-icon-precomposed.png'
def _notify_kodi(self, message, title='SickGear', host=None, username=None, password=None, force=False):
# fill in omitted parameters
if not host:
host = sickbeard.KODI_HOST
if not username:
username = sickbeard.KODI_USERNAME
if not password:
password = sickbeard.KODI_PASSWORD
# suppress notifications if the notifier is disabled but the notify options are checked
if not sickbeard.USE_KODI and not force:
logger.log(u'KODI: Notifications are not enabled, skipping this notification', logger.DEBUG)
return False
result = ''
for curHost in [x.strip() for x in host.split(',')]:
logger.log(u'KODI: Sending Kodi notification to \'%s\' - %s' % (curHost, message), logger.MESSAGE)
command = '{"jsonrpc":"2.0","method":"GUI.ShowNotification","params":{"title":"%s","message":"%s", "image": "%s"},"id":1}' % (title.encode('utf-8'), message.encode('utf-8'), self.sg_logo_url)
notifyResult = self._send_to_kodi(command, curHost, username, password)
if notifyResult:
result += curHost + ':' + notifyResult['result'].decode(sickbeard.SYS_ENCODING)
else:
if sickbeard.KODI_ALWAYS_ON or force:
result += curHost + ':False'
return result
def _send_to_kodi(self, command, host=None, username=None, password=None):
# fill in omitted parameters
if not username:
username = sickbeard.KODI_USERNAME
if not password:
password = sickbeard.KODI_PASSWORD
if not host:
logger.log(u'KODI: No host specified, check your settings', logger.ERROR)
return False
command = command.encode('utf-8')
logger.log(u'KODI: JSON command: ' + command, logger.DEBUG)
url = 'http://%s/jsonrpc' % (host)
try:
req = urllib2.Request(url, command)
req.add_header('Content-type', 'application/json')
# if we have a password, use authentication
if password:
base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
authheader = 'Basic %s' % base64string
req.add_header('Authorization', authheader)
logger.log(u'KODI: Contacting (with auth header) via url: ' + fixStupidEncodings(url), logger.DEBUG)
else:
logger.log(u'KODI: Contacting via url: ' + fixStupidEncodings(url), logger.DEBUG)
try:
response = urllib2.urlopen(req)
except urllib2.URLError, e:
logger.log(u'KODI: Warning: Couldn\'t contact Kodi at ' + host + '- ' + ex(e), logger.WARNING)
return False
# parse the json result
try:
result = json.load(response)
response.close()
logger.log(u'KODI: JSON response: ' + str(result), logger.DEBUG)
return result # need to return response for parsing
except ValueError, e:
logger.log(u'KODI: Unable to decode JSON response: ' + response, logger.WARNING)
return False
except IOError, e:
logger.log(u'KODI: Warning: Couldn\'t contact Kodi at ' + host + ' - ' + ex(e), logger.WARNING)
return False
def _update_library(self, host=None, showName=None):
if not host:
logger.log(u'KODI: No host specified, check your settings', logger.DEBUG)
return False
logger.log(u'KODI: Updating library on host: ' + host, logger.MESSAGE)
# if we're doing per-show
if showName:
tvshowid = -1
logger.log(u'KODI: Updating library for show ' + showName, logger.DEBUG)
# get tvshowid by showName
showsCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShows","id":1}'
showsResponse = self._send_to_kodi(showsCommand, host)
if showsResponse and 'result' in showsResponse and 'tvshows' in showsResponse['result']:
shows = showsResponse['result']['tvshows']
else:
logger.log(u'KODI: No TV shows in Kodi TV show list', logger.DEBUG)
return False
for show in shows:
if (show['label'] == showName):
tvshowid = show['tvshowid']
break # exit out of loop otherwise the label and showname will not match up
# this can be big, so free some memory
del shows
# we didn't find the show (exact match), thus revert to just doing a full update if enabled
if (tvshowid == -1):
logger.log(u'KODI: Exact show name not matched in KODI TV show list', logger.DEBUG)
return False
# lookup tv-show path
pathCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.GetTVShowDetails","params":{"tvshowid":%d, "properties": ["file"]},"id":1}' % (tvshowid)
pathResponse = self._send_to_kodi(pathCommand, host)
path = pathResponse['result']['tvshowdetails']['file']
logger.log(u'KODI: Received Show: ' + showName + ' with ID: ' + str(tvshowid) + ' Path: ' + path, logger.DEBUG)
if (len(path) < 1):
logger.log(u'KODI: No valid path found for ' + showName + ' with ID: ' + str(tvshowid) + ' on ' + host, logger.WARNING)
return False
logger.log(u'KODI: Updating ' + showName + ' on ' + host + ' at ' + path, logger.DEBUG)
updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","params":{"directory":%s},"id":1}' % (json.dumps(path))
request = self._send_to_kodi(updateCommand, host)
if not request:
logger.log(u'KODI: Update of show directory failed on ' + showName + ' on ' + host + ' at ' + path, logger.ERROR)
return False
# catch if there was an error in the returned request
for r in request:
if 'error' in r:
logger.log(u'KODI: Error while attempting to update show directory for ' + showName + ' on ' + host + ' at ' + path, logger.ERROR)
return False
# do a full update if requested
else:
logger.log(u'KODI: Performing full library update on host: ' + host, logger.DEBUG)
updateCommand = '{"jsonrpc":"2.0","method":"VideoLibrary.Scan","id":1}'
request = self._send_to_kodi(updateCommand, host, sickbeard.KODI_USERNAME, sickbeard.KODI_PASSWORD)
if not request:
logger.log(u'KODI: Full library update failed on host: ' + host, logger.ERROR)
return False
return True
def notify_snatch(self, ep_name):
if sickbeard.KODI_NOTIFY_ONSNATCH:
self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_SNATCH])
def notify_download(self, ep_name):
if sickbeard.KODI_NOTIFY_ONDOWNLOAD:
self._notify_kodi(ep_name, common.notifyStrings[common.NOTIFY_DOWNLOAD])
def notify_subtitle_download(self, ep_name, lang):
if sickbeard.KODI_NOTIFY_ONSUBTITLEDOWNLOAD:
self._notify_kodi(ep_name + ': ' + lang, common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD])
def notify_git_update(self, new_version = '??'):
if sickbeard.USE_KODI:
update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT]
title=common.notifyStrings[common.NOTIFY_GIT_UPDATE]
self._notify_kodi(update_text + new_version, title)
def test_notify(self, host, username, password):
return self._notify_kodi('Testing Kodi notifications from SickGear', 'Test', host, username, password, force=True)
def update_library(self, showName=None):
if sickbeard.USE_KODI and sickbeard.KODI_UPDATE_LIBRARY:
if not sickbeard.KODI_HOST:
logger.log(u'KODI: No host specified, check your settings', logger.DEBUG)
return False
# either update each host, or only attempt to update first only
result = 0
for host in [x.strip() for x in sickbeard.KODI_HOST.split(',')]:
if self._update_library(host, showName):
if sickbeard.KODI_UPDATE_ONLYFIRST:
logger.log(u'KODI: Update first host successful on host ' + host + ', stopped sending library update commands', logger.DEBUG)
return True
else:
if sickbeard.KODI_ALWAYS_ON:
result = result + 1
# needed for the 'update kodi' submenu command
# as it only cares of the final result vs the individual ones
if result == 0:
return True
else:
return False
notifier = KODINotifier

View file

@ -1,5 +1,4 @@
# Author: Pedro Correia (http://github.com/pedrocorreia/)
# Based on pushalot.py by Nic Wolfe <nic@wolfeden.ca>
# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickGear.
@ -17,109 +16,123 @@
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import urllib
import urllib2
import socket
import base64
from httplib import HTTPSConnection, HTTPException
import json
from ssl import SSLError
import sickbeard
from sickbeard import logger, common
from sickbeard import logger
from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT
from sickbeard.exceptions import ex
PUSHAPI_ENDPOINT = 'https://api.pushbullet.com/v2/pushes'
DEVICEAPI_ENDPOINT = 'https://api.pushbullet.com/v2/devices'
class PushbulletNotifier:
def test_notify(self, pushbullet_api):
return self._sendPushbullet(pushbullet_api, event="Test", message="Testing Pushbullet settings from SickGear",
method="POST", notificationType="note", force=True)
def get_devices(self, pushbullet_api):
return self._sendPushbullet(pushbullet_api, method="GET", force=True)
def get_devices(self, accessToken=None):
# fill in omitted parameters
if not accessToken:
accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN
def notify_snatch(self, ep_name):
if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH:
self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SNATCH] + " : " + ep_name, message=ep_name,
notificationType="note", method="POST")
# get devices from pushbullet
try:
req = urllib2.Request(DEVICEAPI_ENDPOINT)
base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1]
req.add_header('Authorization', 'Basic %s' % base64string)
handle = urllib2.urlopen(req)
if handle:
result = handle.read()
handle.close()
return result
except urllib2.URLError:
return None
except socket.timeout:
return None
def notify_download(self, ep_name):
if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD:
self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD] + " : " + ep_name,
message=ep_name, notificationType="note", method="POST")
def _sendPushbullet(self, title, body, accessToken, device_iden):
def notify_subtitle_download(self, ep_name, lang):
if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD:
self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + " : " + ep_name + " : " + lang,
message=ep_name + ": " + lang, notificationType="note", method="POST")
def notify_git_update(self, new_version = "??"):
if sickbeard.USE_PUSHBULLET:
update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT]
title=common.notifyStrings[common.NOTIFY_GIT_UPDATE]
self._sendPushbullet(pushbullet_api=None, event=title, message=update_text + new_version, method="POST")
# build up the URL and parameters
body = body.strip().encode('utf-8')
def _sendPushbullet(self, pushbullet_api=None, pushbullet_device=None, event=None, message=None,
notificationType=None, method=None, force=False):
data = urllib.urlencode({
'type': 'note',
'title': title,
'body': body,
'device_iden': device_iden
})
# send the request to pushbullet
try:
req = urllib2.Request(PUSHAPI_ENDPOINT)
base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1]
req.add_header('Authorization', 'Basic %s' % base64string)
handle = urllib2.urlopen(req, data)
handle.close()
except socket.timeout:
return False
except urllib2.URLError, e:
if e.code == 404:
logger.log(u'PUSHBULLET: Access token is wrong/not associated to a device.', logger.ERROR)
elif e.code == 401:
logger.log(u'PUSHBULLET: Unauthorized, not a valid access token.', logger.ERROR)
elif e.code == 400:
logger.log(u'PUSHBULLET: Bad request, missing required parameter.', logger.ERROR)
elif e.code == 503:
logger.log(u'PUSHBULLET: Pushbullet server to busy to handle the request at this time.', logger.WARNING)
return False
logger.log(u'PUSHBULLET: Notification successful.', logger.MESSAGE)
return True
def _notifyPushbullet(self, title, body, accessToken=None, device_iden=None, force=False):
"""
Sends a pushbullet notification based on the provided info or SG config
title: The title of the notification to send
body: The body string to send
accessToken: The access token to grant access
device_iden: The iden of a specific target, if none provided send to all devices
force: If True then the notification will be sent even if Pushbullet is disabled in the config
"""
# suppress notifications if the notifier is disabled but the notify options are checked
if not sickbeard.USE_PUSHBULLET and not force:
return False
if pushbullet_api == None:
pushbullet_api = sickbeard.PUSHBULLET_API
if pushbullet_device == None:
pushbullet_device = sickbeard.PUSHBULLET_DEVICE
# fill in omitted parameters
if not accessToken:
accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN
if not device_iden:
device_iden = sickbeard.PUSHBULLET_DEVICE_IDEN
if method == 'POST':
uri = '/v2/pushes'
else:
uri = '/v2/devices'
logger.log(u'PUSHBULLET: Sending notice with details: \"%s - %s\", device_iden: %s' % (title, body, device_iden), logger.DEBUG)
logger.log(u"Pushbullet event: " + str(event), logger.DEBUG)
logger.log(u"Pushbullet message: " + str(message), logger.DEBUG)
logger.log(u"Pushbullet api: " + str(pushbullet_api), logger.DEBUG)
logger.log(u"Pushbullet devices: " + str(pushbullet_device), logger.DEBUG)
logger.log(u"Pushbullet notification type: " + str(notificationType), logger.DEBUG)
return self._sendPushbullet(title, body, accessToken, device_iden)
http_handler = HTTPSConnection("api.pushbullet.com")
def notify_snatch(self, ep_name):
if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH:
self._notifyPushbullet(notifyStrings[NOTIFY_SNATCH], ep_name)
if notificationType == None:
testMessage = True
try:
logger.log(u"Testing Pushbullet authentication and retrieving the device list.", logger.DEBUG)
http_handler.request(method, uri, None, headers={'Authorization': 'Bearer %s' % pushbullet_api})
except (SSLError, HTTPException, socket.error):
logger.log(u"Pushbullet notification failed.", logger.ERROR)
return False
else:
testMessage = False
try:
data = {
'title': event.encode('utf-8'),
'body': message.encode('utf-8'),
'device_iden': pushbullet_device,
'type': notificationType}
data = json.dumps(data)
http_handler.request(method, uri, body=data,
headers={'Content-Type': 'application/json', 'Authorization': 'Bearer %s' % pushbullet_api})
pass
except (SSLError, HTTPException, socket.error):
return False
def notify_download(self, ep_name):
if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD:
self._notifyPushbullet(notifyStrings[NOTIFY_DOWNLOAD], ep_name)
response = http_handler.getresponse()
request_body = response.read()
request_status = response.status
logger.log(u"Pushbullet response: %s" % request_body, logger.DEBUG)
def notify_subtitle_download(self, ep_name, lang):
if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD:
self._notifyPushbullet(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang)
if request_status == 200:
if testMessage:
return request_body
else:
logger.log(u"Pushbullet notifications sent.", logger.DEBUG)
return True
elif request_status == 410:
logger.log(u"Pushbullet authentication failed: %s" % response.reason, logger.ERROR)
return False
else:
logger.log(u"Pushbullet notification failed.", logger.ERROR)
return False
def notify_git_update(self, new_version = '??'):
if sickbeard.USE_PUSHBULLET:
update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT]
title=notifyStrings[NOTIFY_GIT_UPDATE]
self._notifyPushbullet(title, update_text + new_version)
def test_notify(self, accessToken, device_iden):
return self._notifyPushbullet('Test', 'This is a test notification from SickGear', accessToken, device_iden, force=True)
notifier = PushbulletNotifier

View file

@ -17,23 +17,50 @@
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import httplib
import urllib, urllib2
import urllib
import urllib2
import time
import socket
import base64
import sickbeard
from sickbeard import logger
from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT
from sickbeard.exceptions import ex
API_URL = "https://api.pushover.net/1/messages.json"
API_URL = 'https://api.pushover.net/1/messages.json'
DEVICE_URL = 'https://api.pushover.net/1/users/validate.json'
class PushoverNotifier:
def test_notify(self, userKey=None, apiKey=None):
return self._notifyPushover("This is a test notification from SickGear", 'Test', userKey, apiKey, force=True)
def _sendPushover(self, msg, title, userKey=None, apiKey=None):
def get_devices(self, userKey=None, apiKey=None):
# fill in omitted parameters
if not userKey:
userKey = sickbeard.PUSHOVER_USERKEY
if not apiKey:
apiKey = sickbeard.PUSHOVER_APIKEY
data = urllib.urlencode({
'token': apiKey,
'user': userKey
})
# get devices from pushover
try:
req = urllib2.Request(DEVICE_URL)
handle = urllib2.urlopen(req, data)
if handle:
result = handle.read()
handle.close()
return result
except urllib2.URLError:
return None
except socket.timeout:
return None
def _sendPushover(self, title, msg, userKey, apiKey, priority, device, sound):
"""
Sends a pushover notification to the address provided
@ -44,91 +71,71 @@ class PushoverNotifier:
returns: True if the message succeeded, False otherwise
"""
if userKey == None:
# fill in omitted parameters
if not userKey:
userKey = sickbeard.PUSHOVER_USERKEY
if apiKey == None:
if not apiKey:
apiKey = sickbeard.PUSHOVER_APIKEY
logger.log("Pushover API KEY in use: " + apiKey, logger.DEBUG)
# build up the URL and parameters
msg = msg.strip()
data = urllib.urlencode({
'token': apiKey,
'title': title,
'user': userKey,
'message': msg.encode('utf-8'),
'priority': priority,
'device': device,
'sound': sound,
'timestamp': int(time.time())
})
# send the request to pushover
try:
conn = httplib.HTTPSConnection("api.pushover.net:443")
conn.request("POST", "/1/messages.json",
urllib.urlencode({
"token": apiKey,
"user": userKey,
"title": title.encode('utf-8'),
"message": msg.encode('utf-8'),
'timestamp': int(time.time()),
"retry": 60,
"expire": 3600,
}), {"Content-type": "application/x-www-form-urlencoded"})
except urllib2.HTTPError, e:
# if we get an error back that doesn't have an error code then who knows what's really happening
if not hasattr(e, 'code'):
logger.log("Pushover notification failed." + ex(e), logger.ERROR)
return False
else:
logger.log("Pushover notification failed. Error code: " + str(e.code), logger.ERROR)
req = urllib2.Request(API_URL)
handle = urllib2.urlopen(req, data)
handle.close()
except urllib2.URLError, e:
# HTTP status 404 if the provided email address isn't a Pushover user.
if e.code == 404:
logger.log("Username is wrong/not a pushover email. Pushover will send an email to it", logger.WARNING)
logger.log(u'PUSHOVER: Username is wrong/not a Pushover email. Pushover will send an email to it', logger.WARNING)
return False
# For HTTP status code 401's, it is because you are passing in either an invalid token, or the user has not added your service.
elif e.code == 401:
#HTTP status 401 if the user doesn't have the service added
subscribeNote = self._sendPushover(msg, title, userKey, apiKey)
# HTTP status 401 if the user doesn't have the service added
subscribeNote = self._sendPushover(title, msg, userKey)
if subscribeNote:
logger.log("Subscription send", logger.DEBUG)
logger.log(u'PUSHOVER: Subscription sent', logger.DEBUG)
return True
else:
logger.log("Subscription could not be send", logger.ERROR)
logger.log(u'PUSHOVER: Subscription could not be sent', logger.ERROR)
return False
# If you receive an HTTP status code of 400, it is because you failed to send the proper parameters
elif e.code == 400:
logger.log("Wrong data sent to pushover", logger.ERROR)
logger.log(u'PUSHOVER: Wrong data sent to Pushover', logger.ERROR)
return False
# If you receive a HTTP status code of 429, it is because the message limit has been reached (free limit is 7,500)
elif e.code == 429:
logger.log("Pushover API message limit reached - try a different API key", logger.ERROR)
logger.log(u'PUSHOVER: API message limit reached - try a different API key', logger.ERROR)
return False
logger.log("Pushover notification successful.", logger.MESSAGE)
# If you receive a HTTP status code of 500, service is unavailable
elif e.code == 500:
logger.log(u'PUSHOVER: Unable to connect to API, service unavailable', logger.ERROR)
return False
logger.log(u'PUSHOVER: Notification successful.', logger.MESSAGE)
return True
def notify_snatch(self, ep_name, title=notifyStrings[NOTIFY_SNATCH]):
if sickbeard.PUSHOVER_NOTIFY_ONSNATCH:
self._notifyPushover(title, ep_name)
def notify_download(self, ep_name, title=notifyStrings[NOTIFY_DOWNLOAD]):
if sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD:
self._notifyPushover(title, ep_name)
def notify_subtitle_download(self, ep_name, lang, title=notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD]):
if sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD:
self._notifyPushover(title, ep_name + ": " + lang)
def notify_git_update(self, new_version = "??"):
if sickbeard.USE_PUSHOVER:
update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT]
title=notifyStrings[NOTIFY_GIT_UPDATE]
self._notifyPushover(title, update_text + new_version)
def _notifyPushover(self, title, message, userKey=None, apiKey=None, force=False):
def _notifyPushover(self, title, message, userKey=None, apiKey=None, priority=None, device=None, sound=None, force=False):
"""
Sends a pushover notification based on the provided info or SB config
Sends a pushover notification based on the provided info or SG config
title: The title of the notification to send
message: The message string to send
@ -136,14 +143,46 @@ class PushoverNotifier:
force: Enforce sending, for instance for testing
"""
# suppress notifications if the notifier is disabled but the notify options are checked
if not sickbeard.USE_PUSHOVER and not force:
logger.log("Notification for Pushover not enabled, skipping this notification", logger.DEBUG)
logger.log(u'PUSHOVER: Notifications not enabled, skipping this notification', logger.DEBUG)
return False
logger.log("Sending notification for " + message, logger.DEBUG)
# fill in omitted parameters
if not userKey:
userKey = sickbeard.PUSHOVER_USERKEY
if not apiKey:
apiKey = sickbeard.PUSHOVER_APIKEY
if not priority:
priority = sickbeard.PUSHOVER_PRIORITY
if not device:
device = sickbeard.PUSHOVER_DEVICE
if not sound:
sound = sickbeard.PUSHOVER_SOUND
# self._sendPushover(message, title, userKey, apiKey)
return self._sendPushover(message, title, userKey, apiKey)
logger.log(u'PUSHOVER: Sending notice with details: %s - %s, priority: %s, device: %s, sound: %s' % (title, message, priority, device, sound), logger.DEBUG)
return self._sendPushover(title, message, userKey, apiKey, priority, device, sound)
def test_notify(self, userKey, apiKey, priority, device, sound):
return self._notifyPushover('Test', 'This is a test notification from SickGear', userKey, apiKey, priority, device, sound, force=True)
def notify_snatch(self, ep_name):
if sickbeard.PUSHOVER_NOTIFY_ONSNATCH:
self._notifyPushover(notifyStrings[NOTIFY_SNATCH], ep_name)
def notify_download(self, ep_name):
if sickbeard.PUSHOVER_NOTIFY_ONDOWNLOAD:
self._notifyPushover(notifyStrings[NOTIFY_DOWNLOAD], ep_name)
def notify_subtitle_download(self, ep_name, lang):
if sickbeard.PUSHOVER_NOTIFY_ONSUBTITLEDOWNLOAD:
self._notifyPushover(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang)
def notify_git_update(self, new_version = '??'):
if sickbeard.USE_PUSHOVER:
update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT]
title=notifyStrings[NOTIFY_GIT_UPDATE]
self._notifyPushover(title, update_text + new_version)
notifier = PushoverNotifier

View file

@ -1012,6 +1012,9 @@ class PostProcessor(object):
# do the library update for XBMC
notifiers.xbmc_notifier.update_library(ep_obj.show.name)
# do the library update for Kodi
notifiers.kodi_notifier.update_library(ep_obj.show.name)
# do the library update for Plex
notifiers.plex_notifier.update_library(ep_obj)

View file

@ -380,6 +380,19 @@ def already_postprocessed(dirName, videofile, force):
#Needed if we have downloaded the same episode @ different quality
search_sql = "SELECT tv_episodes.indexerid, history.resource FROM tv_episodes INNER JOIN history ON history.showid=tv_episodes.showid"
search_sql += " WHERE history.season=tv_episodes.season and history.episode=tv_episodes.episode"
np = NameParser(dirName, tryIndexers=True, convert=True)
try:
parse_result = np.parse(dirName)
except:
parse_result = False
pass
if parse_result and (parse_result.show.indexerid and parse_result.episode_numbers and parse_result.season_number):
search_sql += " and tv_episodes.showid = '" + str(parse_result.show.indexerid)\
+ "' and tv_episodes.season = '" + str(parse_result.season_number)\
+ "' and tv_episodes.episode = '" + str(parse_result.episode_numbers[0]) + "'"
search_sql += " and tv_episodes.status IN (" + ",".join([str(x) for x in common.Quality.DOWNLOADED]) + ")"
search_sql += " and history.resource LIKE ?"
sqlResult = myDB.select(search_sql, [u'%' + videofile])

View file

@ -39,7 +39,7 @@ class ProperFinder():
def __init__(self):
self.amActive = False
def run(self, force=False):
def run(self):
if not sickbeard.DOWNLOAD_PROPERS:
return

View file

@ -35,6 +35,7 @@ __all__ = ['ezrss',
'freshontv',
'bitsoup',
'tokyotoshokan',
'animenzb',
]
import sickbeard

View file

@ -0,0 +1,149 @@
# 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 urllib
import datetime
import sickbeard
import generic
from sickbeard import classes, show_name_helpers, helpers
from sickbeard import exceptions, logger
from sickbeard.common import *
from sickbeard import tvcache
from lib.dateutil.parser import parse as parseDate
class animenzb(generic.NZBProvider):
def __init__(self):
generic.NZBProvider.__init__(self, 'animenzb')
self.supportsBacklog = False
self.supportsAbsoluteNumbering = True
self.anime_only = True
self.enabled = False
self.cache = animenzbCache(self)
self.url = 'http://animenzb.com/'
def isEnabled(self):
return self.enabled
def imageName(self):
return 'animenzb.gif'
def _get_season_search_strings(self, ep_obj):
return [x for x in show_name_helpers.makeSceneSeasonSearchString(self.show, ep_obj)]
def _get_episode_search_strings(self, ep_obj, add_string=''):
return [x for x in show_name_helpers.makeSceneSearchString(self.show, ep_obj)]
def _doSearch(self, search_string, search_mode='eponly', epcount=0, age=0):
if self.show and not self.show.is_anime:
logger.log(u'%s is not an anime skipping ...' % self.show.name)
return []
params = {
'cat': 'anime',
'q': search_string.encode('utf-8'),
'max': '100'
}
search_url = self.url + 'rss?' + urllib.urlencode(params)
logger.log(u'Search url: %s' % search_url, logger.DEBUG)
data = self.cache.getRSSFeed(search_url)
if not data:
return []
if 'entries' in data:
items = data.entries
results = []
for curItem in items:
(title, url) = self._get_title_and_url(curItem)
if title and url:
results.append(curItem)
else:
logger.log(
u'The data returned from %s is incomplete, this result is unusable' % self.name,
logger.DEBUG)
return results
return []
def findPropers(self, date=None):
results = []
for item in self._doSearch('v2|v3|v4|v5'):
(title, url) = self._get_title_and_url(item)
if item.has_key('published_parsed') and item['published_parsed']:
result_date = item.published_parsed
if result_date:
result_date = datetime.datetime(*result_date[0:6])
else:
logger.log(u'Unable to figure out the date for entry %s, skipping it' % title)
continue
if not date or result_date > date:
search_result = classes.Proper(title, url, result_date, self.show)
results.append(search_result)
return results
class animenzbCache(tvcache.TVCache):
def __init__(self, provider):
tvcache.TVCache.__init__(self, provider)
self.minTime = 20
def _getRSSData(self):
params = {
'cat': 'anime'.encode('utf-8'),
'max': '100'.encode('utf-8')
}
rss_url = self.provider.url + 'rss?' + urllib.urlencode(params)
logger.log(u'%s cache update URL: %s' % (self.provider.name, rss_url), logger.DEBUG)
data = self.getRSSFeed(rss_url)
if data and 'entries' in data:
return data.entries
else:
return []
provider = animenzb()

View file

@ -218,8 +218,9 @@ class BTNProvider(generic.TorrentProvider):
set(scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]))
for name in name_exceptions:
# Search by name if we don't have tvdb or tvrage id
current_params['series'] = sanitizeSceneName(name)
search_params.append(current_params)
cur_return = current_params.copy()
cur_return['series'] = sanitizeSceneName(name)
search_params.append(cur_return)
return search_params
@ -254,8 +255,9 @@ class BTNProvider(generic.TorrentProvider):
name_exceptions = list(
set(scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]))
for cur_exception in name_exceptions:
search_params['series'] = sanitizeSceneName(cur_exception)
to_return.append(search_params)
cur_return = search_params.copy()
cur_return['series'] = sanitizeSceneName(cur_exception)
to_return.append(cur_return)
return to_return

View file

@ -171,9 +171,10 @@ class NewznabProvider(generic.NZBProvider):
name_exceptions = list(
set(scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]))
for cur_exception in name_exceptions:
if 'q' in cur_params:
cur_params['q'] = helpers.sanitizeSceneName(cur_exception) + '.' + cur_params['q']
to_return.append(cur_params)
cur_return = cur_params.copy()
if 'q' in cur_return:
cur_return['q'] = helpers.sanitizeSceneName(cur_exception) + '.' + cur_return['q']
to_return.append(cur_return)
return to_return
@ -205,8 +206,9 @@ class NewznabProvider(generic.NZBProvider):
name_exceptions = list(
set(scene_exceptions.get_scene_exceptions(ep_obj.show.indexerid) + [ep_obj.show.name]))
for cur_exception in name_exceptions:
params['q'] = helpers.sanitizeSceneName(cur_exception)
to_return.append(params)
cur_return = params.copy()
cur_return['q'] = helpers.sanitizeSceneName(cur_exception)
to_return.append(cur_return)
if ep_obj.show.anime:
# Experimental, add a searchstring without search explicitly for the episode!

View file

@ -15,14 +15,11 @@
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import time
import sickbeard
import generic
from sickbeard import logger
from sickbeard import tvcache
from sickbeard.exceptions import AuthException
class WombleProvider(generic.NZBProvider):
@ -53,8 +50,11 @@ class WombleCache(tvcache.TVCache):
return
cl = []
for url in [self.provider.url + 'rss/?sec=tv-x264&fr=false', self.provider.url + 'rss/?sec=tv-sd&fr=false', self.provider.url + 'rss/?sec=tv-hd&fr=false']:
logger.log(u"Womble's Index cache update URL: " + url, logger.DEBUG)
for url in [self.provider.url + 'rss/?sec=tv-x264&fr=false',
self.provider.url + 'rss/?sec=tv-sd&fr=false',
self.provider.url + 'rss/?sec=tv-dvd&fr=false',
self.provider.url + 'rss/?sec=tv-hd&fr=false']:
logger.log(u'Womble\'s Index cache update URL: ' + url, logger.DEBUG)
data = self.getRSSFeed(url)
# As long as we got something from the provider we count it as an update
@ -65,10 +65,10 @@ class WombleCache(tvcache.TVCache):
for item in data.entries:
title, url = self._get_title_and_url(item)
ci = self._parseItem(title, url)
if ci is not None:
if None is not ci:
cl.append(ci)
if len(cl) > 0:
if 0 < len(cl):
myDB = self._getDB()
myDB.mass_action(cl)
@ -77,6 +77,7 @@ class WombleCache(tvcache.TVCache):
self.setLastUpdate()
def _checkAuth(self, data):
return data != 'Invalid Link'
return 'Invalid Link' != data
provider = WombleProvider()

View file

@ -317,16 +317,11 @@ def _xem_exceptions_fetcher():
continue
for indexerid, names in parsedJSON['data'].items():
xem_exception_dict[int(indexerid)] = names
try:
xem_exception_dict[int(indexerid)] = names
except:
continue
setLastRefresh('xem')
return xem_exception_dict
def getSceneSeasons(indexer_id):
"""get a list of season numbers that have scene exceptions
"""
myDB = db.DBConnection('cache.db')
seasons = myDB.select("SELECT DISTINCT season FROM scene_exceptions WHERE indexer_id = ?", [indexer_id])
return [cur_exception["season"] for cur_exception in seasons]
return xem_exception_dict

View file

@ -77,7 +77,7 @@ class Scheduler(threading.Thread):
if not self.silent:
logger.log(u"Starting new thread: " + self.name, logger.DEBUG)
self.action.run(self.force)
self.action.run()
except Exception, e:
logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR)
logger.log(repr(traceback.format_exc()), logger.DEBUG)

Some files were not shown because too many files have changed in this diff Show more