Merge pull request #1075 from JackDandy/release/0.15.0

Release/0.15.0
This commit is contained in:
JackDandy 2018-03-22 00:02:40 +00:00 committed by GitHub
commit c74ed7a45f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 7274 additions and 1227 deletions

View file

@ -1,8 +1,119 @@
### 0.14.9 (2018-03-19 13:10:00 UTC)
### 0.15.0 (2018-03-22 00:00:00 UTC)
* Add showRSS torrent provider
* Add choice to delete watched episodes from a list of played media at Kodi, Emby, and/or Plex,
instructions at Shows/History/Layout/"Watched"
* Add installable SickGear Kodi repository containing addon "SickGear Watched State Updater"
* Change add Emby setting for watched state scheduler at Config/Notifications/Emby/"Update watched interval"
* Change add Plex setting for watched state scheduler at Config/Notifications/Plex/"Update watched interval"
* Change add map parent folder setting at Notifications for Emby, Kodi, and Plex
* Add API cmd=sg.updatewatchedstate, instructions for use are linked to in layout "Watched" at /history
* Change history page table filter input values are saved across page refreshes
* Change history page table filter inputs, accept values like "dvd or web" to only display both
* Change history page table filter inputs, press 'ESC' key inside a filter input to reset it
* Add provider activity stats to Shows/History/Layout/ drop down
* Change move provider failures table from Manage/Media Search to Shows/History/Layout/Provider fails
* Change sort provider failures by most recent failure, and with paused providers at the top
* Add SickGear-NZBGet dedicated post processing script, see.. \autoProcessTV\SickGear-NG\INSTALL.txt
* Add non standard multi episode name parsing e.g. S01E02and03 and 1x02and03and04
* Change overhaul and add API functions
* Change API version... start with 10
* Change set application response header to 'SickGear' + add API version
* Change return timezone (of network) in API
* Add indexer to calls
* Add SickGear Command tip for old SickBeard commands
* Add warning old sickbeard API calls only support tvdb shows
* Add "tvdbid" fallback only for sickbeard calls
* Add listcommands
* Add list of all commands (old + new) in listcommand page at the beginning
* Change hide 'listcommands' command from commands list, since it needs the API builder CSS + is html not json
* Add missing help in webapi
* Add episode info: absolute_number, scene_season, scene_episode, scene_absolute_number
* Add fork to SB command
* Add sg
* Add sg.activatescenenumbering
* Add sg.addrootdir
* Add sg.checkscheduler
* Add sg.deleterootdir
* Add sg.episode
* Add sg.episode.search
* Add sg.episode.setstatus
* Add sg.episode.subtitlesearch
* Add sg.exceptions
* Add sg.forcesearch
* Add sg.future
* Add sg.getdefaults
* Add sg.getindexericon
* Add sg.getindexers to list all indexers
* Add sg.getmessages
* Add sg.getnetworkicon
* Add sg.getrootdirs
* Add sg.getqualities
* Add sg.getqualitystrings
* Add sg.history
* Add sg.history.clear
* Add sg.history.trim
* Add sg.listtraktaccounts
* Add sg.listignorewords
* Add sg.listrequiedwords
* Add sg.logs
* Add sg.pausebacklog
* Add sg.postprocess
* Add sg.ping
* Add sg.restart
* Add sg.searchqueue
* Add sg.searchtv to search all indexers
* Add sg.setexceptions
* Add sg.setignorewords
* Add sg.setrequiredwords
* Add sg.setscenenumber
* Add sg.show
* Add sg.show.addexisting
* Add sg.show.addnew
* Add sg.show.cache
* Add sg.show.delete
* Add sg.show.getbanner
* Add sg.show.getfanart
* Add sg.show.getposter
* Add sg.show.getquality
* Add sg.show.listfanart
* Add sg.show.ratefanart
* Add sg.show.seasonlist
* Add sg.show.seasons
* Add sg.show.setquality
* Add sg.show.stats
* Add sg.show.refresh
* Add sg.show.pause
* Add sg.show.update
* Add sg.shows
* Add sg.shows.browsetrakt
* Add sg.shows.forceupdate
* Add sg.shows.queue
* Add sg.shows.stats
* Change sickbeard to sickgear
* Change sickbeard_call to property
* Change sg.episode.setstatus allow setting of quality
* Change sg.history, history command output
* Change sg.searchtv to list of indexers
* Add uhd4kweb to qualities
* Add upgrade_once to add existing shows
* Add upgrade_once to add new show
* Add upgrade_once to show quality settings (get/set)
* Add 'ids' to Show + Shows
* Add ids to coming eps + get tvdb id from ids
* Add 'status_str' to coming eps
* Add 'local_datetime' to comming eps + runtime
* Add X-Filename response header to getbanner, getposter
* Add X-Fanartname response header for sg.show.getfanart
* Change remove some non-release group stuff from newnab results
### 0.14.9 (2018-03-19 13:10:00 UTC)
Change remove dead tor caches and stop searching episodes that have a magnet saved
Change AlphaRatio provider freeleech mode; prevent spoiling user ratio from ambiguous filtered results
### 0.14.8 (2018-03-13 22:00:00 UTC)
Fix changing status from "Skipped" to "Wanted" in Manage/Episode Status

View file

@ -0,0 +1,34 @@
SickGear PostProcessing script for NZBGet
=========================================
If NZBGet v17+ is installed on the same system as SickGear then as a local install,
1) Add the location of this script file to NZBGet Settings/PATHS/ScriptDir
2) Navigate to any named TV category at Settings/Categories, click "Choose" Category.Extensions then Apply SickGear-NG
This is the best set up to automatically get script updates from SickGear
#############
If NZBGet v16 or earlier is installed, then as an older install,
1) Copy the directory with/or this single script file to path set in NZBGet Settings/PATHS/ScriptDir
2) Refresh the NZBGet page and navigate to Settings/SickGear-NG
3) Click View -> Compact to remove any tick and un hide tips and suggestions
4) The bare minimum change is the sg_base_path setting or enter `python -m pip install requests` at admin commandline
5) Navigate to any named TV category at Settings/Categories, click "Choose" Category.Extensions then Apply SickGear-NG
You will need to manually update your script with this set up
#############
Notes:
Debian doesn't have pip, _if_ requests is needed, try "apt install python-requests"
-----
Enjoy

View file

@ -0,0 +1,647 @@
#!/usr/bin/env python
#
# ##############################################################################
# ##############################################################################
#
# SickGear PostProcessing script for NZBGet
# =========================================
#
# If NZBGet v17+ is installed on the same system as SickGear then as a local install,
#
# 1) Add the location of this script file to NZBGet Settings/PATHS/ScriptDir
#
# 2) Navigate to any named TV category at Settings/Categories, click "Choose" Category.Extensions then Apply SickGear-NG
#
# This is the best set up to automatically get script updates from SickGear
#
# #############
#
# If NZBGet v16 or earlier is installed, then as an older install,
#
# 1) Copy the directory with/or this single script file to path set in NZBGet Settings/PATHS/ScriptDir
#
# 2) Refresh the NZBGet page and navigate to Settings/SickGear-NG
#
# 3) Click View -> Compact to remove any tick and un hide tips and suggestions
#
# 4) The bare minimum change is the sg_base_path setting or enter `python -m pip install requests` at admin commandline
#
# 5) Navigate to any named TV category at Settings/Categories, click "Choose" Category.Extensions then Apply SickGear-NG
#
# You will need to manually update your script with this set up
#
# ############
#
# Notes:
# Debian doesn't have pip, _if_ requests is needed, try "apt install python-requests"
# -----
# Enjoy
#
# ##############################################################################
# ##############################################################################
#
# Copyright (C) 2016 SickGear Developers
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
### NZBGET QUEUE/POST-PROCESSING SCRIPT ###
### QUEUE EVENTS: NZB_ADDED, NZB_DELETED, URL_COMPLETED, NZB_MARKED ###
# Send PostProcessing requests to SickGear
#
# PostProcessing-Script version: 1.3.
# <!--
# For more info and updates please visit forum topic at
# -->
# <span style="display:block;position:absolute;right:20px;top:105px;width:138px;height:74px;background:url(https://raw.githubusercontent.com/SickGear/SickGear/master/gui/slick/images/sickgear.png)"></span>
# <span style="display:inline-block;margin-top:10px" class="label label-important">
# Setup steps</span> <span class="label label-important" style="display:inline-block;cursor:pointer" data-toggle="modal" href="#InfoDialog">NZBGet Version</span>
# <span style="display:block;color:#666">
# <span style="display:block;padding:4px;margin-top:3px;background-color:#efefef;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px">
# <span style="width:1em;float:left;padding:3px 0 0 3px">
# <span class="label label-important">1</span>
# </span>
# <span style="display:block;margin-left:1.75em;padding:3px 3px 3px 0">
# If <span style="font-weight:bold">NZBGet v17 or newer</span> is installed on the same system as SickGear, then add the
# location of this script file to NZBGet Settings/PATHS/ScriptDir
# <br /><br />
# Or, if <span style="font-weight:bold">NZBGet v16 or earlier</span> is installed on the same system as SickGear and
# if python <a href="https://pypi.python.org/pypi/requests" title="requests library page" target="_blank">requests library</a>
# is not installed, then <strong style="font-weight:bold;color:#128D12 !important">sg_base_path</strong> must be set
# </span>
# </span>
# <span style="display:block;padding:4px;margin-top:3px;background-color:#efefef;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px">
# <span style="width:1em;float:left;padding:3px 0 0 3px">
# <span class="label label-important">2</span>
# </span>
# <span style="display:block;margin-left:1.75em;padding:3px 3px 3px 0">
# Then, for <span style="font-weight:bold">any install</span> type, click <span class="btn" style="padding:1px 5px 0;line-height:16px">Choose</span>
# then apply "<span style="color:#222">SickGear-NG</span>" in a TV Category at NZBGet Settings/CATEGORIES,
# save all changes and reload NZBGet
#
# </span>
# </span>
# </span>
#
# <span class="label label-warning">Note</span> This script requires Python 2.7+ and may not work with Python 3.x+
#
##############################################################################
### OPTIONS ###
#
#test connection@Test SickGear connection
#
# <span class="label label-info">
# Optional</span>
# SickGear <span style="font-weight:bold;color:#128D12 !important">base installation path</span>.
# use where NZBGet v16 or older is installed on the same system as SickGear, and no python requests library is installed
# (use "pip list" to check installed modules)
#sg_base_path=
# <span class="label label-info">
# Optional</span>
# SickGear server ipaddress [default:127.0.0.1 aka localhost].
# change if SickGear is not installed on the same localhost as NZBGet
#sg_host=localhost
# <span class="label label-info">
# Optional</span>
# SickGear HTTP Port [default:8081] (1025-65535).
#sg_port=8081
# <span class="label label-info">
# Optional</span>
# SickGear Username.
#sg_username=
# <span class="label label-info">
# Optional</span>
# SickGear Password.
#sg_password=
# <span class="label label-info">
# Optional</span>
# SickGear has SSL enabled [default:No] (yes, no).
#sg_ssl=no
# <span class="label label-warning">
# Advanced use</span>
# SickGear Web Root.
# change if using a custom SickGear web_root setting (e.g. for a reverse proxy)
#sg_web_root=
# <span class="label label-info">
# Optional</span>
# Print more logging messages [default:No] (yes, no).
# For debugging or if you need to report a bug.
#sg_verbose=no
### NZBGET QUEUE/POST-PROCESSING SCRIPT ###
##############################################################################
import locale
import os
import re
import sys
__version__ = '1.3'
verbose = 0 or 'yes' == os.environ.get('NZBPO_SG_VERBOSE', 'no')
# NZBGet exit codes for post-processing scripts (Queue-scripts don't have any special exit codes).
POSTPROCESS_SUCCESS, POSTPROCESS_ERROR, POSTPROCESS_NONE = 93, 94, 95
failed = False
# define minimum dir size, downloads under this size will be handled as failure
min_dir_size = 20 * 1024 * 1024
class Logger:
INFO, DETAIL, ERROR, WARNING = 'INFO', 'DETAIL', 'ERROR', 'WARNING'
# '[NZB]' send a command message to NZBGet (no log)
NZB = 'NZB'
def __init__(self):
pass
@staticmethod
def safe_print(msg_type, message):
try:
print '[%s] %s' % (msg_type, message.encode(SYS_ENCODING))
except (StandardError, Exception):
try:
print '[%s] %s' % (msg_type, message)
except (StandardError, Exception):
try:
print '[%s] %s' % (msg_type, repr(message))
except (StandardError, Exception):
pass
@staticmethod
def log(message, msg_type=INFO):
size = 900
if size > len(message):
Logger.safe_print(msg_type, message)
else:
for group in (message[pos:pos + size] for pos in xrange(0, len(message), size)):
Logger.safe_print(msg_type, group)
if 'nt' == os.name:
import ctypes
class WinEnv:
def __init__(self):
pass
@staticmethod
def get_environment_variable(name):
name = unicode(name) # ensures string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
env_value = None
if n:
buf = ctypes.create_unicode_buffer(u'\0'*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
env_value = buf.value
verbose and Logger.log('Get var(%s) = %s' % (name, env_value or n))
return env_value
def __getitem__(self, key):
return self.get_environment_variable(key)
def get(self, key, default=None):
r = self.get_environment_variable(key)
return r if r is not None else default
env_var = WinEnv()
else:
class LinuxEnv(object):
def __init__(self, environ):
self.environ = environ
def __getitem__(self, key):
v = self.environ.get(key)
try:
return v.decode(SYS_ENCODING) if isinstance(v, str) else v
except (UnicodeDecodeError, UnicodeEncodeError):
return v
def get(self, key, default=None):
v = self[key]
return v if v is not None else default
env_var = LinuxEnv(os.environ)
SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, '')
except (locale.Error, IOError):
pass
try:
SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError):
pass
if not SYS_ENCODING or SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
SYS_ENCODING = 'UTF-8'
verbose and Logger.log('%s(%s) env dump = %s' % (('posix', 'nt')['nt' == os.name], SYS_ENCODING, os.environ))
class Ek:
def __init__(self):
pass
@staticmethod
def fix_string_encoding(x):
if str == type(x):
try:
return x.decode(SYS_ENCODING)
except UnicodeDecodeError:
return None
elif unicode == type(x):
return x
return None
@staticmethod
def fix_list_encoding(x):
if type(x) not in (list, tuple):
return x
return filter(lambda i: None is not i, map(Ek.fix_string_encoding, i))
@staticmethod
def encode_item(x):
try:
return x.encode(SYS_ENCODING)
except UnicodeEncodeError:
return x.encode(SYS_ENCODING, 'ignore')
@staticmethod
def ek(func, *args, **kwargs):
if 'nt' == os.name:
func_result = func(*args, **kwargs)
else:
func_result = func(*[Ek.encode_item(x) if type(x) == str else x for x in args], **kwargs)
if type(func_result) in (list, tuple):
return Ek.fix_list_encoding(func_result)
elif str == type(func_result):
return Ek.fix_string_encoding(func_result)
return func_result
# Depending on the mode in which the script was called (queue-script NZBNA_DELETESTATUS
# or post-processing-script) a different set of parameters (env. vars)
# is passed. They also have different prefixes:
# - NZBNA in queue-script mode;
# - NZBPP in pp-script mode.
env_run_mode = ('PP', 'NA')['NZBNA_EVENT' in os.environ]
def nzbget_var(name, default='', namespace=env_run_mode):
return env_var.get('NZB%s_%s' % (namespace, name), default)
def nzbget_opt(name, default=''):
return nzbget_var(name, default, 'OP')
def nzbget_plugin_opt(name, default=''):
return nzbget_var('SG_%s' % name, default, 'PO')
sg_path = nzbget_plugin_opt('BASE_PATH')
if not sg_path or not Ek.ek(os.path.isdir, sg_path):
try:
script_path = Ek.ek(os.path.dirname, __file__)
sg_path = Ek.ek(os.path.dirname, Ek.ek(os.path.dirname, script_path))
except (StandardError, Exception):
pass
if sg_path and Ek.ek(os.path.isdir, Ek.ek(os.path.join, sg_path, 'lib')):
sys.path.insert(1, Ek.ek(os.path.join, sg_path, 'lib'))
try:
import requests
except ImportError:
Logger.log('You must set SickGear sg_base_path in script config or install python requests library', Logger.ERROR)
sys.exit(1)
def get_size(start_path='.'):
if Ek.ek(os.path.isfile, start_path):
return Ek.ek(os.path.getsize, start_path)
total_size = 0
for dirpath, dirnames, filenames in Ek.ek(os.walk, start_path):
for f in filenames:
if not f.lower().endswith(('.nzb', '.jpg', '.jpeg', '.gif', '.png', '.tif', '.nfo', '.txt', '.srt', '.sub',
'.sbv', '.idx', '.bat', '.sh', '.exe', '.pdf')):
fp = Ek.ek(os.path.join, dirpath, f)
total_size += Ek.ek(os.path.getsize, fp)
return total_size
def try_int(s, s_default=0):
try:
return int(s)
except (StandardError, Exception):
return s_default
def try_float(s, s_default=0):
try:
return float(s)
except (StandardError, Exception):
return s_default
class ExitReason:
def __init__(self):
pass
PP_SUCCESS = 0
FAIL_SUCCESS = 1
MARKED_BAD_SUCCESS = 2
DELETED = 5
SAME_DUPEKEY = 10
UNFINISHED_DOWNLOAD = 11
NONE = 20
NONE_SG = 21
PP_ERROR = 25
FAIL_ERROR = 26
MARKED_BAD_ERROR = 27
def script_exit(status, reason, runmode=None):
Logger.log('NZBPR_SICKGEAR_PROCESSED=%s_%s_%s' % (status, runmode or env_run_mode, reason), Logger.NZB)
sys.exit(status)
def get_old_status():
old_status = env_var.get('NZBPR_SICKGEAR_PROCESSED', '')
status_regex = re.compile(r'(\d+)_(\w\w)_(\d+)')
if old_status and status_regex.search(old_status) is not None:
s = status_regex.match(old_status)
return try_int(s.group(1)), s.group(2), try_int(s.group(3))
return POSTPROCESS_NONE, env_run_mode, ExitReason.NONE
markbad = 'NZB_MARKED' == env_var.get('NZBNA_EVENT') and 'BAD' == env_var.get('NZBNA_MARKSTATUS')
good_statuses = [(POSTPROCESS_SUCCESS, 'PP', ExitReason.FAIL_SUCCESS), # successfully failed pp'ed
(POSTPROCESS_SUCCESS, 'NA', ExitReason.FAIL_SUCCESS), # queue, successfully failed sent
(POSTPROCESS_SUCCESS, 'NA', ExitReason.MARKED_BAD_SUCCESS)] # queue, mark bad+successfully failed sent
if not markbad:
good_statuses.append((POSTPROCESS_SUCCESS, 'PP', ExitReason.PP_SUCCESS)) # successfully pp'ed
# Start up checks
def start_check():
# Check if the script is called from a compatible NZBGet version (as queue-script or as pp-script)
nzbget_version = re.search(r'^(\d+\.\d+)', nzbget_opt('VERSION', '0.1'))
nzbget_version = nzbget_version.group(1) if nzbget_version and len(nzbget_version.groups()) >= 1 else '0.1'
nzbget_version = try_float(nzbget_version)
if 17 > nzbget_version:
Logger.log('This script is designed to be called from NZBGet 17.0 or later.')
sys.exit(0)
if 'NZB_ADDED' == env_var.get('NZBNA_EVENT'):
Logger.log('NZBPR_SICKGEAR_PROCESSED=', Logger.NZB) # reset var in case of Download Again
sys.exit(0)
# This script processes only certain queue events.
# For compatibility with newer NZBGet versions it ignores event types it doesn't know
if env_var.get('NZBNA_EVENT') not in ['NZB_DELETED', 'URL_COMPLETED', 'NZB_MARKED', None]:
sys.exit(0)
if 'NZB_MARKED' == env_var.get('NZBNA_EVENT') and 'BAD' != env_var.get('NZBNA_MARKSTATUS'):
Logger.log('Marked as [%s], nothing to do, existing' % env_var.get('NZBNA_MARKSTATUS', ''))
sys.exit(0)
old_exit_status = get_old_status()
if old_exit_status in good_statuses and not (
ExitReason.FAIL_SUCCESS == old_exit_status[2] and 'SUCCESS' == nzbget_var('TOTALSTATUS')):
Logger.log('Found result from a previous completed run, exiting')
script_exit(old_exit_status[0], old_exit_status[2], old_exit_status[1])
# If called via "Post-process again" from history details dialog the download may not exist anymore
if 'NZBNA_EVENT' not in os.environ and 'NZBPP_DIRECTORY' in os.environ:
directory = nzbget_var('DIRECTORY')
if not directory or not Ek.ek(os.path.exists, directory):
Logger.log('No files for postprocessor, look back in your NZBGet logs if required, exiting')
script_exit(POSTPROCESS_NONE, ExitReason.NONE)
def call_nzbget_direct(url_command):
# Connect to NZBGet and call an RPC-API method without using python's XML-RPC which is slow for large amount of data
# First we need connection info: host, port and password of NZBGet server, NZBGet passes configuration options to
# scripts using environment variables
host, port, username, password = [nzbget_opt('CONTROL%s' % name) for name in 'IP', 'PORT', 'USERNAME', 'PASSWORD']
url = 'http://%s:%s/jsonrpc/%s' % ((host, '127.0.0.1')['0.0.0.0' == host], port, url_command)
try:
response = requests.get(url, auth=(username, password))
except requests.RequestException:
return ''
return response.content if response.ok else ''
def call_sickgear(nzb_name, dir_name, test=False):
global failed
ssl, host, port, username, password, webroot = [nzbget_plugin_opt(name, default) for name, default in
('SSL', 'no'), ('HOST', 'localhost'), ('PORT', '8081'),
('USERNAME', ''), ('PASSWORD', ''), ('WEB_ROOT', '')]
protocol = 'http%s://' % ('', 's')['yes' == ssl]
webroot = any(webroot) and '/%s' % webroot.strip('/') or ''
url = '%s%s:%s%s/home/postprocess/processEpisode' % (protocol, host, port, webroot)
dupescore = nzbget_var('DUPESCORE')
dupekey = nzbget_var('DUPEKEY')
nzbid = nzbget_var('NZBID')
params = {'nzbName': '%s.nzb' % (nzb_name and re.sub('(?i)\.nzb$', '', nzb_name) or None), 'dir': dir_name,
'failed': int(failed), 'quiet': 1, 'stream': 1, 'force': 1, 'dupekey': dupekey, 'dupescore': dupescore,
'nzbid': nzbid, 'ppVersion': __version__, 'is_basedir': 0, 'client': 'nzbget'}
if test:
params['test'] = '1'
Logger.log('Opening URL: %s with params: %s' % (url, params))
try:
s = requests.Session()
if username or password:
login = '%s%s:%s%s/login' % (protocol, host, port, webroot)
login_params = {'username': username, 'password': password}
s.post(login, data=login_params, stream=True, verify=False)
r = s.get(url, auth=(username, password), params=params, stream=True, verify=False, timeout=900)
except (StandardError, Exception):
Logger.log('Unable to open URL: %s' % url, Logger.ERROR)
return False
success = False
try:
if r.status_code not in [requests.codes.ok, requests.codes.created, requests.codes.accepted]:
Logger.log('Server returned status %s' % str(r.status_code), Logger.ERROR)
return False
for line in r.iter_lines():
if line:
Logger.log(line, Logger.DETAIL)
if test:
if 'Connection success!' in line:
return True
elif not failed and 'Failed download detected:' in line:
failed = True
global markbad
markbad = True
Logger.log('MARK=BAD', Logger.NZB)
success = ('Processing succeeded' in line or 'Successfully processed' in line or
(1 == failed and 'Successful failed download processing' in line))
except Exception as e:
Logger.log(str(e), Logger.ERROR)
return success
def find_dupekey_history(dupekey, nzb_id):
if not dupekey:
return False
data = call_nzbget_direct('history?hidden=true')
cur_status = cur_dupekey = cur_id = ''
cur_dupescore = 0
for line in data.splitlines():
if line.startswith('"NZBID" : '):
cur_id = line[10:-1]
elif line.startswith('"Status" : '):
cur_status = line[12:-2]
elif line.startswith('"DupeKey" : '):
cur_dupekey = line[13:-2]
elif line.startswith('"DupeScore" : '):
cur_dupescore = try_int(line[14:-1])
elif cur_id and line.startswith('}'):
if (cur_status.startswith('SUCCESS') and dupekey == cur_dupekey and
cur_dupescore >= try_int(nzbget_var('DUPESCORE')) and cur_id != nzb_id):
return True
cur_status = cur_dupekey = cur_id = ''
cur_dupescore = 0
return False
def find_dupekey_queue(dupekey, nzb_id):
if not dupekey:
return False
data = call_nzbget_direct('listgroups')
cur_status = cur_dupekey = cur_id = ''
for line in data.splitlines():
if line.startswith('"NZBID" : '):
cur_id = line[10:-1]
elif line.startswith('"Status" : '):
cur_status = line[12:-2]
elif line.startswith('"DupeKey" : '):
cur_dupekey = line[13:-2]
elif cur_id and line.startswith('}'):
if 'PAUSED' != cur_status and dupekey == cur_dupekey and cur_id != nzb_id:
return True
cur_status = cur_dupekey = cur_id = ''
return False
def check_for_failure(directory):
failure = True
dupekey = nzbget_var('DUPEKEY')
if 'PP' == env_run_mode:
total_status = nzbget_var('TOTALSTATUS')
status = nzbget_var('STATUS')
if 'WARNING' == total_status and status in ['WARNING/REPAIRABLE', 'WARNING/SPACE', 'WARNING/DAMAGED']:
Logger.log('WARNING/REPAIRABLE' == status and 'Download is damaged but probably can be repaired' or
'WARNING/SPACE' == status and 'Out of Diskspace' or
'Par-check is required but is disabled in settings', Logger.WARNING)
script_exit(POSTPROCESS_ERROR, ExitReason.UNFINISHED_DOWNLOAD)
elif 'DELETED' == total_status:
Logger.log('Download was deleted and manually processed, nothing to do, exiting')
script_exit(POSTPROCESS_NONE, ExitReason.DELETED)
elif 'SUCCESS' == total_status:
# check for min dir size
if get_size(directory) > min_dir_size:
failure = False
else:
nzb_id = nzbget_var('NZBID')
if (not markbad and find_dupekey_queue(dupekey, nzb_id)) or find_dupekey_history(dupekey, nzb_id):
Logger.log('Download with same Dupekey in download queue or history, exiting')
script_exit(POSTPROCESS_NONE, ExitReason.SAME_DUPEKEY)
nzb_delete_status = nzbget_var('DELETESTATUS')
if nzb_delete_status == 'MANUAL':
Logger.log('Download was manually deleted, exiting')
script_exit(POSTPROCESS_NONE, ExitReason.DELETED)
# Check if it's a Failed Download not added by SickGear
if failure and (not dupekey or not dupekey.startswith('SickGear-')):
Logger.log('Failed download was not added by SickGear, exiting')
script_exit(POSTPROCESS_NONE, ExitReason.NONE_SG)
return failure
# Check if the script is executed from settings page with a custom command
command = os.environ.get('NZBCP_COMMAND')
if None is not command:
if 'test connection' == command:
Logger.log('Test connection...')
result = call_sickgear('', '', test=True)
if True is result:
Logger.log('Connection Test successful!')
sys.exit(POSTPROCESS_SUCCESS)
Logger.log('Connection Test failed!', Logger.ERROR)
sys.exit(POSTPROCESS_ERROR)
Logger.log('Invalid command passed to SickGear-NG: ' + command, Logger.ERROR)
sys.exit(POSTPROCESS_ERROR)
# Script body
def main():
global failed
# Do start up check
start_check()
# Read context (what nzb is currently being processed)
directory = nzbget_var('DIRECTORY')
nzbname = nzbget_var('NZBNAME')
failed = check_for_failure(directory)
if call_sickgear(nzbname, directory):
Logger.log('Successfully post-processed %s' % nzbname)
sys.stdout.flush()
script_exit(POSTPROCESS_SUCCESS,
failed and (markbad and ExitReason.MARKED_BAD_SUCCESS or ExitReason.FAIL_SUCCESS) or
ExitReason.PP_SUCCESS)
Logger.log('Failed to post-process %s' % nzbname, Logger.ERROR)
sys.stdout.flush()
script_exit(POSTPROCESS_ERROR,
failed and (markbad and ExitReason.MARKED_BAD_ERROR or ExitReason.FAIL_ERROR) or
ExitReason.PP_ERROR)
# Execute main script function
main()
script_exit(POSTPROCESS_NONE, ExitReason.NONE)

View file

@ -653,22 +653,32 @@ inc_bottom.tmpl
display:inline
}
#history-table thead th .icon-glyph,
.footer .icon-glyph{
opacity:0.4;filter:alpha(opacity=40);
float:none;
display:inline-block;
margin:0 0 -1px 2px;
height:12px;
margin:0 0 -2px 4px;
height:13px;
width:14px
}
#history-table thead th .icon-glyph:hover,
#history-table tfoot th .icon-glyph:hover,
.footer .icon-glyph:hover{
opacity:0.6;filter:alpha(opacity=60)
}
#history-table thead th .icon-glyph:hover,
.footer .icon-glyph:hover{
opacity:0.6;filter:alpha(opacity=60);
cursor:pointer
}
#history-table thead th .icon-glyph.age,
#history-table thead th .icon-glyph.date:hover,
.footer .icon-glyph.timeleft,
.footer .icon-glyph.time:hover{
background-position:-49px -25px
}
#history-table thead th .icon-glyph.date,
#history-table thead th .icon-glyph.age:hover,
.footer .icon-glyph.time,
.footer .icon-glyph.timeleft:hover{
background-position:-193px -121px
@ -2605,40 +2615,51 @@ h2.day.add-apace, h2.network.add-space{
/* =======================================================================
history.tmpl
========================================================================== */
.strike-deleted{
text-decoration:line-through
}
#historyTable td,
#historyTable td.provider span{
.red-bg{
background-color:#992828
}
.green-bg{
background-color:#3f993f
}
#history-table td,
#history-table td.provider span{
text-align:center
}
#historyTable td.provider span{
#history-table td.provider span{
padding:0 2px
}
#historyTable td.provider > img{
#history-table td.provider > img{
margin-right:3px
}
#historyTable td.provider span.fail img{
#history-table td.provider span.fail img{
opacity:0.5;
filter:alpha(opacity=50)
}
#historyTable td img,
#historyTable td span{
#history-table td img,
#history-table td span{
vertical-align:middle
}
#historyTable td span.article{
#history-table td span.article{
vertical-align:initial
}
#historyTable td img.help,
#historyTable td span.help{
#history-table td img.help,
#history-table td span.help{
cursor:help
}
#historyTable td.tvShow{
#history-table td.tvShow{
text-align:left
}
@ -3256,15 +3277,18 @@ input.get_less_eps{
#media-search .section{
padding-bottom:10px
}
#media-search .btn{
#media-search .btn,
#provider-failures .btn{
margin:0 6px 0 0;
min-width:70px
}
#media-search .btn.shows-more,
#media-search .btn.shows-less{
#media-search .btn.shows-less,
#provider-failures .btn.shows-more,
#provider-failures .btn.shows-less{
margin:6px 6px 6px 0;
}
#media-search .btn.provider-retry{
#provider-failures .btn.provider-retry{
margin:6px 0 6px 4px;
}
.tablesorter.provider-failures{width:auto;clear:both;margin-bottom:10px}
@ -3356,6 +3380,16 @@ input.get_less_eps{
Global
========================================================================== */
.contrast-text,
a.contrast-text:focus,
a.contrast-text:active,
a.contrast-text:visited{
color:#ddd
}
a.contrast-text:hover{
color:#999
}
span.path{
padding:3px;
margin-left:3px

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View file

@ -3,7 +3,9 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>API Builder</title>
<script type="text/javascript" charset="utf-8">
<link rel="stylesheet" type="text/css" href="$sbRoot/css/style.css?v=$sbPID">
<link rel="stylesheet" type="text/css" href="$sbRoot/css/light.css?v=$sbPID">
<script type="text/javascript" charset="utf-8">
<!--
sbRoot = "$sbRoot";
//-->
@ -18,6 +20,8 @@ sbRoot = "$sbRoot";
#apibuilder select option { padding: 1px 6px; line-height: 1.2em; }
#apibuilder .disabled { color: #ccc; }
#apibuilder .action { background-color: #efefef; }
.parareq {display: block; color: red; font-weight: bolder; font-size: 80%;}
.paraopt {display: block; color: gray; font-size: 80%;}
-->
</style>
@ -27,54 +31,201 @@ var disable_empty_list=true;
addListGroup("api", "Command");
addOption("Command", "SickBeard", "?cmd=sb", 1); //make default
addOption("Command", "SickGear", "?cmd=sg", 1); //make default
addOption("Command", "SickBeard", "?cmd=sb");
addOption("Command", "List Commands", "?cmd=listcommands");
addList("Command", "SickBeard.AddRootDir", "?cmd=sb.addrootdir", "sb.addrootdir", "", "", "action");
addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sb.addrootdir", "", "", "action");
addOption("Command", "SickBeard.CheckScheduler", "?cmd=sb.checkscheduler", "", "", "action");
addOption("Command", "SickGear.CheckScheduler", "?cmd=sg.checkscheduler", "", "", "action");
addList("Command", "SickBeard.DeleteRootDir", "?cmd=sb.deleterootdir", "sb.deleterootdir", "", "", "action");
addList("Command", "SickGear.DeleteRootDir", "?cmd=sg.deleterootdir", "sb.deleterootdir", "", "", "action");
addOption("Command", "SickBeard.ForceSearch", "?cmd=sb.forcesearch", "", "", "action");
addList("Command", "SickGear.ForceSearch", "?cmd=sg.forcesearch", "sg.forcesearch", "", "action");
addOption("Command", "SickGear.SearchQueue", "?cmd=sg.searchqueue", "", "", "action");
addOption("Command", "SickBeard.GetDefaults", "?cmd=sb.getdefaults", "", "", "action");
addOption("Command", "SickGear.GetDefaults", "?cmd=sg.getdefaults", "", "", "action");
addOption("Command", "SickBeard.GetMessages", "?cmd=sb.getmessages", "", "", "action");
addOption("Command", "SickGear.GetMessages", "?cmd=sg.getmessages", "", "", "action");
addOption("Command", "SickGear.GetQualities", "?cmd=sg.getqualities", "", "", "action");
addOption("Command", "SickGear.GetQualityStrings", "?cmd=sg.getqualitystrings", "", "", "action");
addList("Command", "SickGear.GetIndexers", "?cmd=sg.getindexers", "listindexers", "", "action");
addList("Command", "SickGear.GetIndexerIcon", "?cmd=sg.getindexericon", "getindexericon", "", "action");
addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon", "", "action");
addOption("Command", "SickBeard.GetRootDirs", "?cmd=sb.getrootdirs", "", "", "action");
addOption("Command", "SickGar.GetRootDirs", "?cmd=sg.getrootdirs", "", "", "action");
addList("Command", "SickBeard.PauseBacklog", "?cmd=sb.pausebacklog", "sb.pausebacklog", "", "", "action");
addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog", "", "", "action");
addOption("Command", "SickBeard.Ping", "?cmd=sb.ping", "", "", "action");
addOption("Command", "SickGear.Ping", "?cmd=sg.ping", "", "", "action");
addOption("Command", "SickBeard.Restart", "?cmd=sb.restart", "", "", "action");
addOption("Command", "SickGear.Restart", "?cmd=sg.restart", "", "", "action");
addList("Command", "SickBeard.SearchTVDB", "?cmd=sb.searchtvdb", "sb.searchtvdb", "", "", "action");
addList("Command", "SickGear.SearchTV", "?cmd=sg.searchtv", "sg.searchtv", "", "", "action");
addList("Command", "SickBeard.SetDefaults", "?cmd=sb.setdefaults", "sb.setdefaults", "", "", "action");
addOption("Command", "SickBeard.Shutdown", "?cmd=sb.shutdown", "", "", "action");
addList("Command", "SickGear.ListIgnoreWords", "?cmd=sg.listignorewords", "listignorewords", "", "action");
addList("Command", "SickGear.SetIgnoreWords", "?cmd=sg.setignorewords", "setwords", "", "action");
addList("Command", "SickGear.ListRequiredWords", "?cmd=sg.listrequiredwords", "listrequiredwords", "", "action");
addList("Command", "SickGear.SetRequiredWords", "?cmd=sg.setrequiredwords", "setwords", "", "action");
addList("Command", "Coming Episodes", "?cmd=future", "future");
addList("Command", "SickGear - Coming Episodes", "?cmd=sg.future", "future");
addList("Command", "Episode", "?cmd=episode", "episode");
addList("Command", "SickGear.Episode", "?cmd=sg.episode", "sg.episode");
addList("Command", "Episode.Search", "?cmd=episode.search", "episode.search", "", "", "action");
addList("Command", "SickGear.Episode.Search", "?cmd=sg.episode.search", "sg.episode.search", "", "", "action");
addList("Command", "Episode.SetStatus", "?cmd=episode.setstatus", "episode.setstatus", "", "", "action");
addList("Command", "SickGear.Episode.SetStatus", "?cmd=sg.episode.setstatus", "sg.episode.setstatus", "", "", "action");
addList("Command", "Scene Exceptions", "?cmd=exceptions", "exceptions");
addList("Command", "SickGear Scene Exceptions", "?cmd=sg.exceptions", "sg.exceptions");
addList("Command", "Set Scene Exceptions", "?cmd=sg.setexceptions", "changeexceptions", "", "action");
addList("Command", "Set Scene Number", "?cmd=sg.setscenenumber", "setscenenumber", "", "action");
addList("Command", "Activate Scene Numbering", "?cmd=sg.activatescenenumbering", "activatescenenumber", "", "action");
addList("Command", "History", "?cmd=history", "history");
addList("Command", "SickGear History", "?cmd=sg.history", "history");
addOption("Command", "History.Clear", "?cmd=history.clear", "", "", "action");
addOption("Command", "SickGear.History.Clear", "?cmd=sg.history.clear", "", "", "action");
addOption("Command", "History.Trim", "?cmd=history.trim", "", "", "action");
addOption("Command", "SickGear.History.Trim", "?cmd=sg.history.trim", "", "", "action");
addList("Command", "PostProcess", "?cmd=postprocess", "postprocess", "", "","action");
addList("Command", "SickGear PostProcess", "?cmd=sg.postprocess", "postprocess", "", "","action");
addList("Command", "Logs", "?cmd=logs", "logs");
addList("Command", "SickGear Logs", "?cmd=sg.logs", "logs");
addList("Command", "Show", "?cmd=show", "indexerid");
addList("Command", "SickGear.Show", "?cmd=sg.show", "sg.indexerid");
addList("Command", "Show.AddExisting", "?cmd=show.addexisting", "show.addexisting", "", "", "action");
addList("Command", "SickGear.Show.AddExisting", "?cmd=sg.show.addexisting", "sg.show.addexisting", "", "", "action");
addList("Command", "Show.AddNew", "?cmd=show.addnew", "show.addnew", "", "", "action");
addList("Command", "SickGear.Show.AddNew", "?cmd=sg.show.addnew", "sg.show.addnew", "", "", "action");
addList("Command", "Show.Cache", "?cmd=show.cache", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Cache", "?cmd=sg.show.cache", "sg.indexerid", "", "", "action");
addList("Command", "Show.Delete", "?cmd=show.delete", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Delete", "?cmd=sg.show.delete", "sg.indexerid", "", "", "action");
addList("Command", "Show.GetBanner", "?cmd=show.getbanner", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.GetBanner", "?cmd=sg.show.getbanner", "sg.indexerid", "", "", "action");
addList("Command", "SickGear.Show.ListFanart", "?cmd=sg.show.listfanart", "sg.indexerid", "", "", "action");
addList("Command", "SickGear.Show.GetFanart", "?cmd=sg.show.getfanart", "sg.indexerid", "", "", "action");
addList("Command", "Show.GetPoster", "?cmd=show.getposter", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.GetPoster", "?cmd=sg.show.getposter", "sg.indexerid", "", "", "action");
addList("Command", "Show.GetQuality", "?cmd=show.getquality", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.GetQuality", "?cmd=sg.show.getquality", "sg.indexerid", "", "", "action");
addList("Command", "Show.Pause", "?cmd=show.pause", "show.pause", "", "", "action");
addList("Command", "SickGear.Show.Pause", "?cmd=sg.show.pause", "sg.show.pause", "", "", "action");
addList("Command", "Show.Refresh", "?cmd=show.refresh", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Refresh", "?cmd=sg.show.refresh", "sg.indexerid", "", "", "action");
addList("Command", "Show.SeasonList", "?cmd=show.seasonlist", "show.seasonlist", "", "", "action");
addList("Command", "SickGear.Show.SeasonList", "?cmd=sg.show.seasonlist", "sg.show.seasonlist", "", "", "action");
addList("Command", "Show.Seasons", "?cmd=show.seasons", "seasons", "", "", "action");
addList("Command", "SickGear.Show.Seasons", "?cmd=sg.show.seasons", "sg.seasons", "", "", "action");
addList("Command", "Show.SetQuality", "?cmd=show.setquality", "show.setquality", "", "", "action");
addList("Command", "SickGear.Show.SetQuality", "?cmd=sg.show.setquality", "sg.show.setquality", "", "", "action");
addList("Command", "Show.Stats", "?cmd=show.stats", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Stats", "?cmd=sg.show.stats", "sg.indexerid", "", "", "action");
addList("Command", "Show.Update", "?cmd=show.update", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Update", "?cmd=sg.show.update", "sg.indexerid", "", "", "action");
addList("Command", "Shows", "?cmd=shows", "shows");
addList("Command", "SickGear.Shows", "?cmd=sg.shows", "shows");
addList("Command", "SickGear.Shows.BrowseTrakt", "?cmd=sg.shows.browsetrakt", "sg.shows.browsetrakt");
addOption("Command", "Shows.ListTraktAccounts", "?cmd=sg.listtraktaccounts", "", "", "action");
addOption("Command", "Shows.Stats", "?cmd=shows.stats", "", "", "action");
addOption("Command", "SickGear.Shows.Stats", "?cmd=sg.shows.stats", "", "", "action");
addOption("Command", "SickGear.Shows.ForceUpdate", "?cmd=sg.shows.forceupdate", "", "", "action");
addOption("Command", "SickGear.Shows.Queue", "?cmd=sg.shows.queue", "", "", "action");
addOption("sg.shows.browsetrakt", "Anticipated", "&type=anticipated");
addOption("sg.shows.browsetrakt", "New Shows", "&type=newshows");
addOption("sg.shows.browsetrakt", "New Seasons", "&type=newseasons");
addOption("sg.shows.browsetrakt", "Popular", "&type=popular");
addOption("sg.shows.browsetrakt", "Trending", "&type=trending");
addList("sg.shows.browsetrakt", "Recommended", "&type=recommended", "sg.traktaccounts");
addList("sg.shows.browsetrakt", "Watchlist", "&type=watchlist", "sg.traktaccounts");
#from sickbeard import TRAKT_ACCOUNTS
#for $a in $TRAKT_ACCOUNTS
addOption("sg.traktaccounts", "$TRAKT_ACCOUNTS[$a].name", "&account_id=$a");
#end for
addOption("sg.forcesearch", "Recent Search", "&searchtype=recent", 1);
addOption("sg.forcesearch", "Backlog Search", "&searchtype=backlog");
addOption("sg.forcesearch", "Proper Search", "&searchtype=proper");
addOption("listindexers", "Optional Param", "", 1);
addOption("listindexers", "Searchable only", "&searchable-only=1");
addOption("getnetworkicon", "CBS", "&network=cbs");
addOption("getnetworkicon", "NBC", "&network=nbc");
addOption("getnetworkicon", "Youtube", "&network=youtube");
addOption("getnetworkicon", "The CW", "&network=the%20cw");
addOption("getnetworkicon", "Crime & Investigation Network", "&network=crime%20%26%20investigation%20network");
#for $i in $indexers
addList("getindexericon", "$indexers[$i]", "&indexer=$i", "");
#end for
#for $curShow in $sortedShowList:
addList("activatescenenumber", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer", "scenenumber-active");
#end for
addOption("scenenumber-active", "Activate", "&activate=1");
addOption("scenenumber-active", "Deactivate", "&activate=0");
#for $curShow in $sortedShowList:
addList("setscenenumber", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer", "setscene-fseason");
#end for
addOption("listignorewords", "Optional Param", "", 1);
#for $curShow in $sortedShowList:
addOption("listignorewords", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer");
#end for
addList("setwords", "Optional Param", "", "addwords");
#for $curShow in $sortedShowList:
addList("setwords", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer", "addwords");
#end for
addList("addwords", "Optional Param", "", "removewords");
addList("addwords", "ignore1", "&add=ignore1", "removewords");
addList("addwords", "ignore2, ignore3", "&add=ignore2|ignore3", "removewords");
addList("removewords", "Optional Param", "", "useregex");
addList("removewords", "ignore1", "&remove=ignore1", "useregex");
addList("removewords", "ignore2", "&remove=ignore2", "useregex");
addList("removewords", "ignore2, ignore3", "&remove=ignore2|ignore3", "useregex");
addOption("useregex", "Optional Param", "", 1);
addOption("useregex", "as Regex", "&regex=1");
addOption("useregex", "as Words", "&regex=0");
addOption("listrequiredwords", "Optional Param", "", 1);
#for $curShow in $sortedShowList:
addOption("listrequiredwords", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer");
#end for
addList("setscene-fseason", "forSeason", "", "setscene-fepisode", 1);
addList("setscene-fepisode", "forEpisode", "", "setscene-fabsolute", 1);
addList("setscene-fabsolute", "forAbsolute", "", "setscene-season");
addList("setscene-season", "sceneSeason", "", "setscene-episode", 1);
addList("setscene-episode", "sceneEpisode", "", "setscene-absolute", 1);
addList("setscene-absolute", "sceneAbsolute", "", "");
#for $c in range(1, 11):
addList("setscene-fseason", "$c", "&forSeason=$c", "setscene-fepisode");
addList("setscene-fepisode", "$c", "&forEpisode=$c", "setscene-fabsolute");
addList("setscene-fabsolute", "$c", "&forAbsolute=$c", "setscene-season");
addList("setscene-season", "$c", "&sceneSeason=$c", "setscene-episode");
addList("setscene-episode", "$c", "&sceneEpisode=$c", "setscene-absolute");
addList("setscene-absolute", "$c", "&sceneAbsolute=$c", "");
#end for
// addOption("indexerid", "Optional Param", "", 1);
#for $curShow in $sortedShowList:
addOption("indexerid", "$curShow.name", "&indexerid=$curShow.indexerid");
#end for
#for $curShow in $sortedShowList:
addOption("sg.indexerid", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid");
#end for
addOption("logs", "Optional Param", "", 1);
addOption("logs", "Debug", "&min_level=debug");
addOption("logs", "Info", "&min_level=info");
@ -109,20 +260,26 @@ addOption("shows", "Show Only Not Paused", "&paused=0");
addOption("shows", "Sort by Show Name", "&sort=name");
addOption("shows", "Sort by TVDB ID", "&sort=id");
addList("show.addexisting", "C:\\temp\\show1", "&location=C:\\temp\\show1", "show.addexisting-indexerid");
addList("show.addexisting", "D:\\Temp\\show2", "&location=D:\\Temp\\show2", "show.addexisting-indexerid");
addList("show.addexisting", "S:\\TV\\Ancient Aliens", "&location=S:\\TV\\Ancient Aliens", "show.addexisting-indexerid");
addList("show.addexisting", "S:\\TV\\Chuck", "&location=S:\\TV\\Chuck", "show.addexisting-indexerid");
addList("sg.show.addexisting", "101501 (Ancient Aliens)", "&indexer=1&indexerid=101501", "show.addexisting-loc");
addList("sg.show.addexisting", "80348 (Chuck)", "&indexer=1&indexerid=80348", "show.addexisting-loc");
addList("show.addexisting-indexerid", "101501 (Ancient Aliens)", "&indexerid=101501", "show.addexisting-opt");
addList("show.addexisting-indexerid", "80348 (Chuck)", "&indexerid=80348", "show.addexisting-opt");
addList("show.addexisting", "101501 (Ancient Aliens)", "&tvdbid=101501", "show.addexisting-loc");
addList("show.addexisting", "80348 (Chuck)", "&tvdbid=80348", "show.addexisting-loc");
addList("show.addexisting-loc", "C:\\temp\\show1", "&location=C:\\temp\\show1", "show.addexisting-opt");
addList("show.addexisting-loc", "D:\\Temp\\show2", "&location=D:\\Temp\\show2", "show.addexisting-opt");
addList("show.addexisting-loc", "S:\\TV\\Ancient Aliens", "&location=S:\\TV\\Ancient Aliens", "show.addexisting-opt");
addList("show.addexisting-loc", "S:\\TV\\Chuck", "&location=S:\\TV\\Chuck", "show.addexisting-opt");
addOption("show.addexisting-opt", "Optional Param", "", 1);
addList("show.addexisting-opt", "No Season Folder", "&season_folder=0", "quality");
addList("show.addexisting-opt", "Use Season Folder", "&season_folder=1", "quality");
addList("show.addnew", "101501 (Ancient Aliens)", "&indexerid=101501", "show.addnew-loc");
addList("show.addnew", "80348 (Chuck)", "&indexerid=80348", "show.addnew-loc");
addList("show.addnew", "101501 (Ancient Aliens)", "&tvdbid=101501", "show.addnew-loc");
addList("show.addnew", "80348 (Chuck)", "&tvdbid=80348", "show.addnew-loc");
addList("sg.show.addnew", "101501 (Ancient Aliens)", "&indexer=1&indexerid=101501", "show.addnew-loc");
addList("sg.show.addnew", "80348 (Chuck)", "&indexer=1&indexerid=80348", "show.addnew-loc");
addOption("show.addnew-loc", "Optional Param", "", 1);
addList("show.addnew-loc", "C:\\Temp", "&location=C:\\temp", "show.addnew-status");
@ -141,52 +298,56 @@ addList("show.addnew-opt", "No Season Folder", "&season_folder=0", "quality");
addList("show.addnew-opt", "Use Season Folder", "&season_folder=1", "quality");
addOptGroup("sb.searchtvdb", "Search by Name");
addList("sb.searchtvdb", "Lost", "&name=Lost", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "office", "&name=office", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "OffiCE", "&name=OffiCE", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "Leno", "&name=leno", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "Top Gear", "&name=Top Gear", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "Lost", "&name=Lost", "");
addList("sb.searchtvdb", "office", "&name=office", "");
addList("sb.searchtvdb", "OffiCE", "&name=OffiCE", "");
addList("sb.searchtvdb", "Leno", "&name=leno", "");
addList("sb.searchtvdb", "Top Gear", "&name=Top Gear", "");
endOptGroup("sb.searchtvdb");
addOptGroup("sb.searchtvdb", "Search by indexerid");
addList("sb.searchtvdb", "73739", "&indexerid=73739", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "74608", "&indexerid=74608", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "199051", "&indexerid=199051", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "123456 (invalid show)", "&indexerid=123456", "sb.searchtvdb-lang");
addList("sb.searchtvdb", "73739", "&indexerid=73739", "");
addList("sb.searchtvdb", "74608", "&indexerid=74608", "");
addList("sb.searchtvdb", "199051", "&indexerid=199051", "");
addList("sb.searchtvdb", "123456 (invalid show)", "&indexerid=123456", "");
endOptGroup("sb.searchtvdb");
addOption("sb.searchtvdb-lang", "Optional Param", "", 1);
addOption("sb.searchtvdb-lang", "Chinese", "&lang=zh"); // 27
addOption("sb.searchtvdb-lang", "Croatian", "&lang=hr"); // 31
addOption("sb.searchtvdb-lang", "Czech", "&lang=cs"); // 28
addOption("sb.searchtvdb-lang", "Danish", "&lang=da"); // 10
addOption("sb.searchtvdb-lang", "Dutch", "&lang=nl"); // 13
addOption("sb.searchtvdb-lang", "English", "&lang=en"); // 7
addOption("sb.searchtvdb-lang", "Finnish", "&lang=fi"); // 11 -- Suomeksi
addOption("sb.searchtvdb-lang", "French", "&lang=fr"); // 17
addOption("sb.searchtvdb-lang", "German", "&lang=de"); // 14
addOption("sb.searchtvdb-lang", "Greek", "&lang=el"); // 20
addOption("sb.searchtvdb-lang", "Hebrew", "&lang=he"); // 24
addOption("sb.searchtvdb-lang", "Hungarian", "&lang=hu"); // 19 -- Magyar
addOption("sb.searchtvdb-lang", "Italian", "&lang=it"); // 15
addOption("sb.searchtvdb-lang", "Japanese", "&lang=ja"); // 25
addOption("sb.searchtvdb-lang", "Korean", "&lang=ko"); // 32
addOption("sb.searchtvdb-lang", "Norwegian", "&lang=no"); // 9
addOption("sb.searchtvdb-lang", "Polish", "&lang=pl"); // 18
addOption("sb.searchtvdb-lang", "Portuguese", "&lang=pt");// 26
addOption("sb.searchtvdb-lang", "Russian", "&lang=ru"); // 22
addOption("sb.searchtvdb-lang", "Slovenian", "&lang=sl"); // 30
addOption("sb.searchtvdb-lang", "Spanish", "&lang=es"); // 16
addOption("sb.searchtvdb-lang", "Swedish", "&lang=sv"); // 8
addOption("sb.searchtvdb-lang", "Turkish", "&lang=tr"); // 21
addOptGroup("sg.searchtv", "Search by Name");
addList("sg.searchtv", "Lost", "&name=Lost", "indexertosearch");
addList("sg.searchtv", "office", "&name=office", "indexertosearch");
addList("sg.searchtv", "OffiCE", "&name=OffiCE", "indexertosearch");
addList("sg.searchtv", "Leno", "&name=leno", "indexertosearch");
addList("sg.searchtv", "Top Gear", "&name=Top Gear", "indexertosearch");
endOptGroup("sg.searchtv");
addOptGroup("sg.searchtv", "Search by indexerid");
addList("sg.searchtv", "73739", "&indexerid=73739", "indexertosearch");
addList("sg.searchtv", "74608", "&indexerid=74608", "indexertosearch");
addList("sg.searchtv", "199051", "&indexerid=199051", "indexertosearch");
addList("sg.searchtv", "123456 (invalid show)", "&indexerid=123456", "indexertosearch");
endOptGroup("sg.searchtv");
addOption("indexertosearch", "Optional Param", "", 1);
addOption("indexertosearch", "All Indexers", "&indexers=-1");
#for $i in $searchindexers
addOption("indexertosearch", "$searchindexers[$i]", "&indexers=$i");
#end for
#for $curShow in $sortedShowList:
addList("seasons", "$curShow.name", "&indexerid=$curShow.indexerid", "seasons-$curShow.indexerid");
#end for
#for $curShow in $sortedShowList:
addList("sg.seasons", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "seasons-$curShow.indexerid");
#end for
#for $curShow in $sortedShowList:
addList("show.seasonlist", "$curShow.name", "&indexerid=$curShow.indexerid", "show.seasonlist-sort");
#end for
#for $curShow in $sortedShowList:
addList("sg.show.seasonlist", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "show.seasonlist-sort");
#end for
addOption("show.seasonlist-sort", "Optional Param", "", 1);
addOption("show.seasonlist-sort", "Sort by Ascending", "&sort=asc");
@ -194,6 +355,10 @@ addOption("show.seasonlist-sort", "Sort by Ascending", "&sort=asc");
addList("show.setquality", "$curShow.name", "&indexerid=$curShow.indexerid", "quality");
#end for
#for $curShow in $sortedShowList:
addList("sg.show.setquality", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "quality");
#end for
//build out generic quality options
addOptGroup("quality", "Quality Templates");
addOption("quality", "SD", "&initial=sdtv|sddvd");
@ -243,6 +408,10 @@ addOption("seasons-$curShow", "$curShowSeason.season", "&season=$curShowSeason.s
#end for
#end for
#for $curShow in $sortedShowList:
addList("sg.episode", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "episode-$curShow.indexerid");
#end for
#for $curShow in $sortedShowList:
addList("episode", "$curShow.name", "&indexerid=$curShow.indexerid", "episode-$curShow.indexerid");
#end for
@ -250,10 +419,15 @@ addList("episode", "$curShow.name", "&indexerid=$curShow.indexerid", "episode-$c
// build out each show's season+episode list for episode cmd
#for $curShow in $episodeSQLResults:
#for $curShowSeason in $episodeSQLResults[$curShow]:
addList("episode-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-$curShow-full");
addList("episode-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-full");
#end for
addOption("episode-$curShow-full", "Optional Param", "", 1);
addOption("episode-$curShow-full", "Show Full Path", "&full_path=1");
#end for
addOption("episode-full", "Optional Param", "", 1);
addOption("episode-full", "Show Full Path", "&full_path=1");
// build out tvshow list for sg.episode.search
#for $curShow in $sortedShowList:
addList("sg.episode.search", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "episode.search-$curShow.indexerid");
#end for
// build out tvshow list for episode.search
@ -268,6 +442,31 @@ addOption("episode.search-$curShow", "$curShowSeason.season x $curShowSeason.epi
#end for
#end for
// build out tvshow list for sg.episode.setstatus
#for $curShow in $sortedShowList:
addList("sg.episode.setstatus", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "sg.episode.setstatus-$curShow.indexerid");
#end for
#for $curShow in $episodeSQLResults:
#for $curShowSeason in $episodeSQLResults[$curShow]:
addList("sg.episode.setstatus-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "sg.episode-status");
#end for
#end for
addList("sg.episode-status", "Wanted", "&status=wanted", "sg.episode.quality");
addList("sg.episode-status", "Skipped", "&status=skipped", "sg.episode.quality");
addList("sg.episode-status", "Archived", "&status=archived", "sg.episode.quality");
addList("sg.episode-status", "Ignored", "&status=ignored", "sg.episode.quality");
addList("sg.episode-status", "Snatched", "&status=snatched", "sg.episode.quality");
addList("sg.episode-status", "Downloaded", "&status=downloaded", "sg.episode.quality");
#from sickbeard.webapi import quality_map
addOption("sg.episode.quality", "Opt Param", 1);
#for $q in $quality_map:
addOption("sg.episode.quality", "$q", "&quality=$q");
#end for
// build out tvshow list for episode.setstatus
#for $curShow in $sortedShowList:
addList("episode.setstatus", "$curShow.name", "&indexerid=$curShow.indexerid", "episode.setstatus-$curShow.indexerid");
@ -276,13 +475,13 @@ addList("episode.setstatus", "$curShow.name", "&indexerid=$curShow.indexerid", "
// build out each show's season+episode list for episode.setstatus cmd
#for $curShow in $episodeSQLResults:
#for $curShowSeason in $episodeSQLResults[$curShow]:
addList("episode.setstatus-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-status-$curShow");
addList("episode.setstatus-$curShow", "$curShowSeason.season x $curShowSeason.episode", "&season=$curShowSeason.season&episode=$curShowSeason.episode", "episode-status");
#end for
addOption("episode-status-$curShow", "Wanted", "&status=wanted");
addOption("episode-status-$curShow", "Skipped", "&status=skipped");
addOption("episode-status-$curShow", "Archived", "&status=archived");
addOption("episode-status-$curShow", "Ignored", "&status=ignored");
#end for
addOption("episode-status", "Wanted", "&status=wanted");
addOption("episode-status", "Skipped", "&status=skipped");
addOption("episode-status", "Archived", "&status=archived");
addOption("episode-status", "Ignored", "&status=ignored");
addOption("future", "Optional Param", "", 1);
addList("future", "Sort by Date", "&sort=date", "future-type");
@ -324,6 +523,28 @@ addOption("exceptions", "Optional Param", "", 1);
addOption("exceptions", "$curShow.name", "&indexerid=$curShow.indexerid");
#end for
addOption("sg.exceptions", "Optional Param", "", 1);
#for $curShow in $sortedShowList:
addOption("sg.exceptions", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid");
#end for
#for $curShow in $sortedShowList:
addList("changeexceptions", "$curShow.name", "&indexerid=$curShow.indexerid&indexer=$curShow.indexer", "exceptionforseason");
#end for
addList("exceptionforseason", "All Seasons", "&forseason=-1", "addexceptions");
#for $c in range(1, 11):
addList("exceptionforseason", "All Seasons", "&forseason=$c", "addexceptions");
#end for
addList("addexceptions", "Optional Param", "", "removeexceptions");
addList("addexceptions", "Test Name 1", "&add=Test Name 1", "removeexceptions");
addList("addexceptions", "Test Name 1, Name 2", "&add=Test Name 1|Name 2", "removeexceptions");
addOption("removeexceptions", "Optional Param", "", 1);
addOption("removeexceptions", "Test Name 1", "&remove=Test Name 1");
addOption("removeexceptions", "Test Name 1, Name 2", "&remove=Test Name 1|Name 2");
addOption("sb.pausebacklog", "Optional Param", "", 1);
addOption("sb.pausebacklog", "Pause", "&pause=1");
addOption("sb.pausebacklog", "Unpause", "&pause=0");
@ -343,6 +564,9 @@ addOption("sb.deleterootdir", "S:\\Invalid_Location", "&location=S:\\Invalid_Loc
#for $curShow in $sortedShowList:
addList("show.pause", "$curShow.name", "&indexerid=$curShow.indexerid", "show.pause-opt");
#end for
#for $curShow in $sortedShowList:
addList("sg.show.pause", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "show.pause-opt");
#end for
addOption("show.pause-opt", "Optional Param", "", 1);
addOption("show.pause-opt", "Unpause", "&pause=0");
addOption("show.pause-opt", "Pause", "&pause=1");
@ -350,7 +574,7 @@ addOption("show.pause-opt", "Pause", "&pause=1");
</script>
</head>
<body onload="initListGroup('api', document.apibuilder.firstlevel, document.apibuilder.secondlevel, document.apibuilder.thirdlevel, document.apibuilder.forthlevel, document.apibuilder.fifthlevel, document.apibuilder.sixthlevel, document.apibuilder.seventhlevel)">
<body style="padding-top: 0 !important;" onload="initListGroup('api', document.apibuilder.firstlevel, document.apibuilder.secondlevel, document.apibuilder.thirdlevel, document.apibuilder.forthlevel, document.apibuilder.fifthlevel, document.apibuilder.sixthlevel, document.apibuilder.seventhlevel, document.apibuilder.eigthlevel)">
<form name="apibuilder" id="apibuilder" action="">
<table align="center">
@ -372,9 +596,10 @@ addOption("show.pause-opt", "Pause", "&pause=1");
<select name="fifthlevel"><option></option></select>
<select name="sixthlevel"><option></option></select>
<select name="seventhlevel"><option></option></select>
<select name="eigthlevel"><option></option></select>
<div style="float: left; ">
<input class="btn" type="button" value="Reset" onclick="resetListGroup('api',1)" />
<input class="btn" type="button" value="Go" onclick="goListGroup(this.form['apikey'].value, this.form['seventhlevel'].value, this.form['sixthlevel'].value, this.form['fifthlevel'].value, this.form['forthlevel'].value, this.form['thirdlevel'].value, this.form['secondlevel'].value, this.form['firstlevel'].value)" />
<input class="btn" type="button" value="Go" onclick="goListGroup(this.form['apikey'].value, this.form['eigthlevel'].value, this.form['seventhlevel'].value, this.form['sixthlevel'].value, this.form['fifthlevel'].value, this.form['forthlevel'].value, this.form['thirdlevel'].value, this.form['secondlevel'].value, this.form['firstlevel'].value)" />
</div>
</td>
</tr>

View file

@ -364,12 +364,12 @@
<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"#echo ('', $checked)[$sg_var('FUZZY_DATING') == True]#>
<input type="checkbox" name="fuzzy_dating" id="fuzzy_dating" class="view-if datePresets"#echo ('', $checked)[$sg_var('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#echo (' metadataDiv', '')[$sg_var('FUZZY_DATING')]#">
<div class="field-pair show-if-fuzzy_dating#echo (' metadataDiv', '')[$sg_var('FUZZY_DATING')]#">
<label for="trim_zero">
<span class="component-title">Trim date and time</span>
<span class="component-desc">
@ -481,7 +481,7 @@
<span class="component-title">API enable</span>
<span class="component-desc">
<input type="checkbox" name="use_api" class="enabler" id="use_api"#echo ('', $checked)[$sg_var('USE_API')]#>
<p>permit the use of the SickGear (SickBeard) API</p>
<p>permit the use of the SickGear (and Legacy SickBeard) API</p>
</span>
</label>
</div>
@ -490,6 +490,7 @@
<label for="api_key">
<span class="component-title">API key</span>
<span class="component-desc">
<p>The legacy SickBeard API is limited to shows from thetvdb.com.<br>Use the SickGear API endpoint for full access</p>
<input type="text" name="api_key" id="api_key" value="$sg_str('API_KEY')" class="form-control input-sm input300" readonly="readonly">
<input class="btn btn-inline" type="button" id="generate_new_apikey" value="Generate">
<div class="clear-left"><p>used to give 3rd party programs limited access to SickGear</p></div>

View file

@ -4,6 +4,8 @@
#from lib.libtrakt import TraktAPI
#from sickbeard.helpers import anon_url, starify
#from sickbeard.notifiers import NotifierFactory
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
#set global $title = 'Config - Notifications'
#set global $header = 'Notifications'
@ -91,6 +93,32 @@
</span>
</label>
</div>
#set $selected = ' selected="selected" class="selected"'
<div class="field-pair">
<label for="emby-watched-interval">
<span class="component-title">Update watched interval</span>
<span class="component-desc">
<select id="emby-watched-interval" name="emby_watched_interval" class="form-control input-sm view-if">
<option value="0"#if not $sg_var('EMBY_WATCHEDSTATE_SCHEDULED')#${selected}#end if#>Off </option>
#for v in [10, 15, 30, 45, 60]
<option value="$v"#if $sg_var('EMBY_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('EMBY_WATCHEDSTATE_FREQUENCY')#${selected}#end if#>$v #if not $sg_var('EMBY_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('EMBY_WATCHEDSTATE_FREQUENCY')#(recent) #end if#</option>
#end for
</select>
<span>minutes to fetch episode watched states for the history page</span>
</span>
</label>
</div>
<div class="field-pair show-if-emby-watched-interval">
<label for="emby-watched-mapped">
<span class="component-title">Map parent folder(s)</span>
<span class="component-desc">
<input type="text" name="emby_parent_maps" id="emby-watched-mapped" value="$sickbeard.EMBY_PARENT_MAPS" class="form-control input-sm input250">
<p>(comma separated)</p>
<div class="clear-left"><p>link each Emby library folder that differs to its SickGear parent folder counterpart<br>
(e.g. d:\tvstuff = c:\tv, /mnt/media/tv = /tv, /emby/folder = /sickgear/parent/folder)</p></div>
</span>
</label>
</div>
<div class="field-pair">
<label for="emby-host">
<span class="component-title">Host(s) running Emby</span>
@ -174,6 +202,17 @@
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi-watched-mapped">
<span class="component-title">Map parent folder(s)</span>
<span class="component-desc">
<input type="text" name="kodi_parent_maps" id="kodi-watched-mapped" value="$sickbeard.KODI_PARENT_MAPS" class="form-control input-sm input250">
<p>(comma separated)</p>
<div class="clear-left"><p>link each Kodi library folder that differs to its SickGear parent folder counterpart<br>
(e.g. d:\tvstuff = c:\tv, /mnt/media/tv = /tv, /kodi/folder = /sickgear/parent/folder)</p></div>
</span>
</label>
</div>
<div class="field-pair">
<label for="kodi-host">
<span class="component-title">Host(s) running Kodi</span>
@ -285,30 +324,54 @@
<label for="plex-update-library">
<span class="component-title">Update server library</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="plex_update_library" id="plex-update-library" #if $sickbeard.PLEX_UPDATE_LIBRARY then 'checked="checked" ' else ''#/>
<input type="checkbox" name="plex_update_library" id="plex-update-library" #if $sickbeard.PLEX_UPDATE_LIBRARY then 'checked="checked" ' else ''#/>
<p>update Plex Media Server library when a download finishes</p>
</span>
</label>
</div>
<div id="content_plex-update-library">
<div class="field-pair">
<label for="plex-server-host">
<span class="component-title">Plex Media Server IP:Port</span>
<span class="component-desc">
<input type="text" name="plex_server_host" id="plex-server-host" value="<%= re.sub(r'\b,\b', ', ', sickbeard.PLEX_SERVER_HOST) %>" class="form-control input-sm input350">
<div class="clear-left">
<p>one or more hosts running Plex Media Server<br>(eg. 192.168.1.1:32400, 192.168.1.2:32400)</p>
</div>
</span>
</label>
</div>
#set $selected = ' selected="selected" class="selected"'
<div class="field-pair">
<label for="plex-watched-interval">
<span class="component-title">Update watched interval</span>
<span class="component-desc">
<select id="plex-watched-interval" name="plex_watched_interval" class="form-control input-sm view-if">
<option value="0"#if not $sg_var('PLEX_WATCHEDSTATE_SCHEDULED')#${selected}#end if#>Off </option>
#for v in [10, 15, 30, 45, 60]
<option value="$v"#if $sg_var('PLEX_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('PLEX_WATCHEDSTATE_FREQUENCY')#${selected}#end if#>$v #if not $sg_var('PLEX_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('PLEX_WATCHEDSTATE_FREQUENCY')#(recent) #end if#</option>
#end for
</select>
<span>minutes to fetch episode watched states for the history page</span>
</span>
</label>
</div>
<div class="field-pair show-if-plex-watched-interval">
<label for="plex-watched-mapped">
<span class="component-title">Map parent folder(s)</span>
<span class="component-desc">
<input type="text" name="plex_parent_maps" id="plex-watched-mapped" value="$sickbeard.PLEX_PARENT_MAPS" class="form-control input-sm input250">
<p>(comma separated)</p>
<div class="clear-left"><p>link each Plex library folder that differs to its SickGear parent folder counterpart<br>
(e.g. d:\tvstuff = c:\tv, /mnt/media/tv = /tv, /plex/folder = /sickgear/parent/folder)</p></div>
</span>
</label>
</div>
<div class="field-pair">
<label for="plex-server-host">
<span class="component-title">Plex Media Server IP:Port</span>
<span class="component-desc">
<input type="text" name="plex_server_host" id="plex-server-host" value="<%= re.sub(r'\b,\b', ', ', sickbeard.PLEX_SERVER_HOST) %>" class="form-control input-sm input350">
<div class="clear-left">
<p>one or more hosts running Plex Media Server<br>(eg. 192.168.1.1:32400, 192.168.1.2:32400)</p>
</div>
</span>
</label>
</div>
<div class="field-pair">
<div class="test-notification" id="test-pms-result">Click below to test Plex server(s)</div>
<input class="btn" type="button" value="Test Plex Server" id="test-pms">
<input type="submit" class="config_submitter btn" value="Save Changes">
<div class="clear-left">&nbsp;</div>
</div>
<div class="field-pair">
<div class="test-notification" id="test-pms-result">Click below to test Plex server(s)</div>
<input class="btn" type="button" value="Test Plex Server" id="test-pms">
<input type="submit" class="config_submitter btn" value="Save Changes">
<div class="clear-left">&nbsp;</div>
</div>
</div>
@ -1693,12 +1756,12 @@
<label for="slack-as-authed">
<span class="component-title">Post as authed user</span>
<span class="component-desc">
<input type="checkbox" class="viewIf" name="slack_as_authed" id="slack-as-authed" #if $sickbeard.SLACK_AS_AUTHED then 'checked="checked"' else ''#>
<input type="checkbox" class="view-if" name="slack_as_authed" id="slack-as-authed" #if $sickbeard.SLACK_AS_AUTHED then 'checked="checked"' else ''#>
<p>send notifications using the profile name and photo of the access token</p>
</span>
</label>
</div>
<div class="hide_if_slack-as-authed">
<div class="hide-if-slack-as-authed">
<div class="field-pair">
<label for="slack-bot-name">
<span class="component-title">Post as a custom name</span>
@ -1791,12 +1854,12 @@
<label for="discordapp-as-authed">
<span class="component-title">Post as authed user</span>
<span class="component-desc">
<input type="checkbox" class="viewIf" name="discordapp_as_authed" id="discordapp-as-authed" #if $sickbeard.DISCORDAPP_AS_AUTHED then 'checked="checked"' else ''#>
<input type="checkbox" class="view-if" name="discordapp_as_authed" id="discordapp-as-authed" #if $sickbeard.DISCORDAPP_AS_AUTHED then 'checked="checked"' else ''#>
<p>send notifications using the username and avatar of the channel webhook</p>
</span>
</label>
</div>
<div class="hide_if_discordapp-as-authed">
<div class="hide-if-discordapp-as-authed">
<div class="field-pair">
<label for="discordapp-username">
<span class="component-title">Post as a custom name</span>

View file

@ -114,7 +114,7 @@
<span class="component-title">Extra scripts</span>
<span class="component-desc">
<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" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_extra_params" title="Toggle info for script arguments">
<div class="clear-left">
<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
@ -332,7 +332,7 @@
<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">
<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" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend">
</span>
</label>
</div>
@ -552,7 +552,7 @@
<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">
<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" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend">
</span>
</label>
</div>
@ -750,7 +750,7 @@
<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">
<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" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_sports_key" title="Toggle Sports Naming Legend">
</span>
</label>
</div>
@ -953,7 +953,7 @@
<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">
<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" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_anime_key" title="Toggle Anime Naming Legend">
</span>
</label>
</div>
@ -1264,4 +1264,4 @@
//-->
</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

@ -82,7 +82,6 @@
<span class="component-desc">
<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>
<p><em class="grey-text">enter 4489 for experimental internal provider frequencies</em></p>
</span>
</label>
</div>
@ -98,16 +97,16 @@
</div>
<div class="field-pair">
<label for="backlog_nofull">
<label for="backlog-nofull">
<span class="component-title">Disable auto full backlog</span>
<span class="component-desc">
<input type="checkbox" name="backlog_nofull" id="backlog_nofull" class="enabler viewIf"<%= html_checked if sickbeard.BACKLOG_NOFULL == True else '' %>>
<input type="checkbox" name="backlog_nofull" id="backlog-nofull" class="enabler view-if"<%= html_checked if sickbeard.BACKLOG_NOFULL == True else '' %>>
<p>backlog search manually by setting episodes 'Wanted' or via 'Backlog overview'</p>
</span>
</label>
</div>
<div class="field-pair" id="content_backlog_nofull">
<div class="field-pair" id="content_backlog-nofull">
<label>
<span class="component-title"><em class="grey-text">Backlog search spread</em></span>
<span class="component-desc">
@ -115,7 +114,7 @@
</span>
</label>
</div>
<div class="field-pair hide_if_backlog_nofull">
<div class="field-pair hide-if-backlog-nofull">
<label>
<span class="component-title">Backlog search spread</span>
<span class="component-desc">

View file

@ -1,64 +1,41 @@
#import datetime
#import re
##
#import sickbeard
#from sickbeard import history, providers, sbdatetime
#from sickbeard import history, providers, sbdatetime, WEB_PORT
#from sickbeard.common import Quality, statusStrings, SNATCHED_ANY, SNATCHED_PROPER, DOWNLOADED, SUBTITLED, ARCHIVED, FAILED
#from sickbeard.helpers import human
#from sickbeard.providers import generic
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
#set global $title = 'History'
#set global $header = 'History'
#set $layout = $sg_str('HISTORY_LAYOUT', 'detailed')
#set $layout_name = 'watched' in $layout and 'Watched' or 'stats' in $layout and 'Activity Hits' or 'provider_failures'in $layout and 'Provider Failures' or 'Activity'
#set sg_port = str($getVar('sbHttpPort', WEB_PORT))
#set global $title = 'History : %s' % $layout_name
#set global $header = 'History <span class="grey-text">: %s</span>' % $layout_name
#set global $sbPath = '..'
#set global $topmenu = 'home'
#set $layout = $sg_str('HISTORY_LAYOUT', 'detailed')
##
#import os.path
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl')
##
#set $checked = ' checked="checked"'
<script type="text/javascript">
<script src="$sbRoot/js/history.js?v=$sbPID"></script>
<script>
<!--
\$.tablesorter.addParser({
id: 'cDate',
is: function(s) {
return false;
},
format: function(s) {
return s;
},
type: 'numeric'
});
\$.SickGear.history = {
layoutName: '$layout',
isCompact: #echo ('!1', '!0')['compact' in $layout]#,
isTrashit: #echo ('!1', '!0')[bool($sg_var('TRASH_REMOVE_SHOW'))]#,
useSubtitles: #echo ('!1', '!0')[bool($sg_var('USE_SUBTITLES'))]#,
lastDeleteFiles: '#echo ('', $checked)[$getVar('last_delete_files', False)]#',
lastDeleteRecords: '#echo ('', $checked)[$getVar('last_delete_records', False)]#',
};
\$(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[data-name]').attr('data-name').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
}
});
\$('#limit').change(function(){
window.location.href = '$sbRoot/history/?limit=' + \$(this).val()
});
\$(document).ready(function() {
#set $fuzzydate = 'airdate'
#if $sg_var('FUZZY_DATING')
@ -80,31 +57,64 @@
#else
<h1 class="title">$title</h1>
#end if
#if $varExists('earliest')
<div class="grey-text" style="clear:both;margin:-8px 0 1.5em 2px;font-size:0.85em;float:left">
Stats range from <span class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($earliest), $history.dateFormat))</span> until <span class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($latest), $history.dateFormat))</span>
</div>
#end if
#set $html_selected = ' selected="selected"'
#set $selected = ' selected="selected" class="selected"'
##
<div class="h2footer pull-right">Limit:
<div id="results-sortby" class="h2footer pull-right">Layout:
<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>
<option value="100"#echo ('', $selected)['100' == $limit]#>100</option>
<option value="250"#echo ('', $selected)['250' == $limit]#>250</option>
<option value="500"#echo ('', $selected)['500' == $limit]#>500</option>
<option value="0"#echo ('', $selected)['0' == $limit]#>All</option>
</select>
<span style="margin-left:5px">Layout:
<span style="margin-left:5px">
<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' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed"#echo ('', $html_selected)['detailed' == $sg_str('HISTORY_LAYOUT', 'detailed')]#>Detailed</option>
<optgroup label="Activity">
<option value="$sbRoot/setHistoryLayout/?layout=compact"#echo ('', $selected)['compact' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed"#echo ('', $selected)['detailed' == $sg_str('HISTORY_LAYOUT', 'detailed')]#>Detailed</option>
</optgroup>
<optgroup label="Watched">
<option value="$sbRoot/setHistoryLayout/?layout=compact_watched"#echo ('', $selected)['compact_watched' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed_watched"#echo ('', $selected)['detailed_watched' == $sg_str('HISTORY_LAYOUT')]#>Detailed</option>
</optgroup>
<optgroup label="Stats">
<option value="$sbRoot/setHistoryLayout/?layout=compact_stats"#echo ('', $selected)['compact_stats' == $sg_str('HISTORY_LAYOUT')]#>Activity hits</option>
<option value="$sbRoot/setHistoryLayout/?layout=graph_stats"#echo ('', $selected)['graph_stats' == $sg_str('HISTORY_LAYOUT')]#>Graphed hits</option>
<option value="$sbRoot/setHistoryLayout/?layout=provider_failures"#echo ('', $selected)['provider_failures' == $sg_str('HISTORY_LAYOUT')]#>Provider fails</option>
</optgroup>
</select>
</span>
</div>
<br>
<table id="historyTable" class="sickbeardTable tablesorter $layout" cellspacing="1" border="0" cellpadding="0">
<style>
#watched-help thead tr th, #watched-help tbody tr td{text-align:left}
#watched-help tbody td ol, #watched-help tbody td ul, #watched-help tbody td p{margin-bottom:0}
#watched-help tbody td img{margin-right:3px}
#watched-help .vmid{vertical-align:middle}
#history-table .age{display:none}
#history-table.event-age .age{display:inline-block}
#history-table.event-age .date{display:none}
</style>
##
#if 'failure' not in $layout
##
<table id="history-table" data-table-group="$layout" class="sickbeardTable tablesorter $layout" cellspacing="1" border="0" cellpadding="0">
##
#end if
##
##
#if 'detailed' == $layout
##
##
<thead>
<tr>
<th class="nowrap">Time</th>
<th class="text-nowrap">Time</th>
<th width="35%">Episode</th>
<th>Action</th>
<th>Provider</th>
@ -114,21 +124,20 @@
<tfoot>
<tr>
<th class="nowrap" colspan="5">&nbsp;</th>
<th class="text-nowrap" colspan="5">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $historyResults
#for $hItem in $history_results
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem['action']))
#set $data_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')]
#set $display_name = '<span data-name="%s">%s - S%02iE%02i</span>' % (
$data_name,
(re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')],
int(hItem['season']), int(hItem['episode']))
#set $display_name = '<span data-sort="%s">%s - S%02iE%02i</span>' % (
$hItem['data_name'],
(('<span class="article">%s</span> %s' % ($hItem['name1'], $hItem['name2'])), $hItem['show_name'])[$sg_var('SORT_ARTICLE') or not $hItem['name1']],
int($hItem['season']), int($hItem['episode']))
<tr>
#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><div class="${fuzzydate}" data-sort="$time.mktime($curdatetime.timetuple())">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div></td>
<td class="tvShow"><a href="$sbRoot/home/displayShow?show=$hItem['showid']#season-$hItem['season']">$display_name#if $Quality.splitCompositeStatus($hItem['action'])[0] == $SNATCHED_PROPER then ' <span class="quality Proper">Proper</span>' else ''#</a></td>
<td#echo ('', ' class="subtitles_column"')[$SUBTITLED == $curStatus]#>
#if $SUBTITLED == $curStatus
@ -159,11 +168,14 @@
<td><span class="hide">$curQuality</span><span class="quality $Quality.get_quality_css($curQuality)">$Quality.get_quality_ui($curQuality)</span></td>
</tr>
#end for
#else
##
##
#elif ('compact' == $layout)
##
##
<thead>
<tr>
<th class="nowrap">Time</th>
<th class="text-nowrap">Time</th>
<th width="#echo '3%s%%' % ('5', '0')[$sg_var('USE_SUBTITLES')]#">Episode</th>
<th>Snatched</th>
<th>Downloaded</th>
@ -176,18 +188,17 @@
<tfoot>
<tr>
<th class="nowrap" colspan="6">&nbsp;</th>
<th class="text-nowrap" colspan="6">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $compactResults
#for $hItem in $compact_results
#set $curdatetime = $datetime.datetime.strptime(str($hItem['actions'][0]['time']), $history.dateFormat)
#set $data_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')]
#set $display_name = '<span data-name="%s">%s - S%02iE%02i</span>' % (
$data_name,
(re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')],
int(hItem['season']), int(hItem['episode']))
#set $display_name = '<span data-sort="%s">%s - S%02iE%02i</span>' % (
$hItem['data_name'],
(('<span class="article">%s</span> %s' % ($hItem['name1'], $hItem['name2'])), $hItem['show_name'])[$sg_var('SORT_ARTICLE') or not $hItem['name1']],
int($hItem['season']), int($hItem['episode']))
#set $prov_list = []
#set $down_list = []
#set $order = 1
@ -204,8 +215,8 @@
$provider.name, $basename)]
#set $order += (0, 1)[$curStatus in $SNATCHED_ANY]
#else
#set $prov_list += ['<img src="%s/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider" />'\
% $sbRoot]
#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 $curStatus in [$DOWNLOADED, $ARCHIVED]
@ -235,7 +246,7 @@
#end if
#end for
<tr>
<td><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort-data">$time.mktime($curdatetime.timetuple())</span></td>
<td><div class="${fuzzydate}" data-sort="$time.mktime($curdatetime.timetuple())">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div></td>
<td class="tvShow">
<span><a href="$sbRoot/home/displayShow?show=$hItem['show_id']#season-$hItem['season']">$display_name#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' <span class="quality Proper">Proper</span>' else ''#</a></span>
</td>
@ -261,9 +272,408 @@
<td quality="$curQuality"><span class="quality $Quality.get_quality_css($curQuality)">$Quality.get_quality_ui($curQuality)</span></td>
</tr>
#end for
##
##
#elif 'watched' in $layout
##
##
<thead>
<tr>
<th><span class="date">Event Date</span><span class="age">Event Age</span></th>
<th width="8%">Played</th>
<th class="text-nowrap">Episode</th>
<th class="text-nowrap" width="15%">Label (Profile)</th>
<th class="text-nowrap" width="10%">Quality</th>
<th class="text-nowrap" width="10%">Size</th>
<th width="10%">Delete</th>
</tr>
</thead>
#end if
<tfoot>
<tr>
<th>
<i style="background-image:url($sbRoot/images/legend16.png)" id="show-watched-help" title="Toggle help" class="add-qtip icon-glyph"></i>
<span id="row-count" style="font-size:12px;line-height:20px;float:left"></span>
</th>
<th colspan="4"></th>
<th><span id="sum-size" style="border-top:solid 1px #ddd">0 Bytes</span></th>
<th>
#if $len($results)
<input id="del-watched" type="button" class="btn" value="Submit">
#end if
</th>
</tr>
</tfoot>
<tbody id="tbody">
#if not $results
<tr colspan="7">
<td colspan="7" style="text-align:center">
<p>Media marked watched or unwatched will list in this space</p>
</td>
</tr>
#else
#for $hItem in $results
#if $hItem.hide
#continue
#end if
#set $compact = 'compact' in $layout and $hItem['rowid'] not in $mru_row_ids
##
#set $curdatetime = $datetime.datetime.fromtimestamp($hItem.get('date_watched'))
#set $curage = ($datetime.datetime.now() - $curdatetime).days
#set $display_name = '<span data-sort="%s"%s>%s - S%sE%s</span>' % (
$hItem.get('data_name'),
('', ' class="grey-text"')[$hItem.get('deleted')],
(('<span class="article">%s</span> %s' % ($hItem.get('name1'), $hItem.get('name2'))), $hItem.get('show_name'))[$sg_var('SORT_ARTICLE') or not $hItem.get('name1')],
$hItem.get('season'), $hItem.get('episode'))
<tr data-tvep-id="$hItem['tvep_id']" data-file="$hItem['location']"#if $compact# class="hide"#end if#>
<td>
<div class="date ${fuzzydate}" data-sort="$curage">$sbdatetime.sbdatetime.sbfdatetime($curdatetime)</div>
<div class="age">${curage}d</div>
</td>
<td>
#set $float_played = int($hItem.get('played'))/100.0
#set $value = ($float_played, int($float_played))[int($float_played) == $float_played]
<span#if not $bool($int($value))# class="add-qtip" title="Marked Unwatched"#end if#>$value</span>
</td>
<td class="tvShow text-nowrap">
<span class="add-qtip#if not $hItem.get('deleted')#"#else# strike-deleted" title="file no longer exists"#end if#>
<a href="$sbRoot/home/displayShow?show=$hItem.get('showid')#season-$hItem.get('season')">$display_name</a>
</span>
</td>
<td>
#set $label = re.sub('\{[^}]+\}$', '', $hItem.get('label'))
#set $client = ''
#try
#set $client = re.findall('\{([^}]+)\}$', $hItem.get('label'))[0].lower()
#set $client_label = ('%s %s' % ($client, $label)).strip(' ')
<img height="16px" style="margin-right:3px" src="$sbRoot/images/notifiers/${client}.png"><span data-sort="$client_label" style="vertical-align:middle">$label</span>
#except
<span data-sort="${label}">$label</span>
#pass
#end try
</td>
<td quality="$hItem.get('quality')" class="text-nowrap">
<span class="quality $Quality.get_quality_css($hItem.get('quality')) add-qtip" title="#if $hItem.get('deleted')#file no longer exists#else#$hItem['location']#end if#">$Quality.qualityStrings[$hItem.get('quality')].replace('SD DVD', 'SD DVD/BR/BD')</span>
</td>
<td class="size text-nowrap">
<span#if $hItem.get('deleted')# class="add-qtip grey-text strike-deleted" title="file no longer exists"#end if# data-sort="$hItem.get('file_size')">$human($hItem.get('file_size'))</span>
</td>
<td class="#echo ('green', 'red')[100 > $hItem.get('mru_count')]#-bg">
<input id="del-$hItem.get('rowid')-$hItem.get('indexer')-$hItem.get('showid')"
title="last&nbsp;event<br>#echo ('watched', 'unwatched')[100 > $hItem.get('mru_count')]#"
type="checkbox" class="del-check add-qtip">
#if $hItem.get('mru_count')
<span style="position:absolute">
<i class="icon-glyph" style="top:-6px;right:4px;position:relative; opacity: 0.4;background-position: -95px -119px"></i>
</span>
#end if
</td>
</tr>
#end for
#end if
</tbody>
</table>
#def row_class()
#set global $row += 1
#echo ('even', 'odd')[bool($row % 2)]
#end def
<table id="watched-help" style="#if $hide_watched_help#display:none;#end if#margin-top:15px" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead>
<tr>
<th colspan="2">General help</th>
</tr>
</thead>
#set global $row = 0
<tbody>
<tr class="$row_class()">
<td colspan="2">
<p>Filters are saved per layout. Examples;</p>
<ul>
<li>Event Date/Age, or Played: <span class="grey-text">&gt;7 and &lt;60</span> (between 7d and 60d) , <span class="grey-text">&gt;1</span> (played more than once)</li>
<li>Label (Profile): <span class="grey-text">emby or kodi</span> , <span class="grey-text">!kodi and !plex</span> , <span class="grey-text">emby user2</span> , <span class="grey-text">emby user"</span> (single end quote excludes user2)</li>
<li>Quality: <span class="grey-text">sd or dl</span> , <span class="grey-text">blu</span></li>
</ul>
</td
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>The above table is sorted first by played and then by date of event (i.e. watched/unwatched)</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>To multi-select checkboxes or column headers, click then hold shift and click</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p style="margin-bottom:6px">Key for <span class="grey-text">Delete</span> column;</p>
<ul>
<li style="line-height:25px"><span class="contrast-text green-bg" style="padding:4px 18px 0 6px;margin:0 6px 0 0">Green
<span style="position:absolute">
<i class="icon-glyph" style="position:relative;top:-2px;right:8px;opacity: 0.4;background-position:-95px -119px"></i>
</span>
</span>Watched at least once at client</li>
<li style="line-height:25px"><span class="contrast-text red-bg" style="padding:4px 20px 0 6px;margin:0 6px 0 0">Red
</span>Partially watched or set 'unwatched' at client
</li>
</ul>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>To find how much freespace a delete will yield, the size tally increases for selected episodes that have a media file</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>A mapping in the client notification section is needed for results if a player library folder is different to a parent folder</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>In <span class="grey-text">Compact</span> layout, deleting records removes all episode related records. <span class="grey-text">Detailed</span> layout allows for individual selection [<a rel="dialog" href="https://raw.githubusercontent.com/wiki/SickGear/SickGear/images/screenies/watched.png">Show me</a>]</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>Any script can add to the watched list by making a <a href="https://github.com/SickGear/SickGear/wiki/API#sg.updatewatchedstate">documented API call</a> to <code>sg.updatewatchedstate</code></p>
</td>
</tr>
</tbody>
<thead>
<tr>
<th width="20%">Supported clients</th>
<th>To use</th>
</tr>
</thead>
#set global $row = 0
<tbody>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/kodi.png"><span class="vmid">Kodi</span>
<p><em class="grey-text">Isengard, Jarvis, Krypton</em><br>
Episodes marked watched or unwatched are pushed in real-time and shown above.</p>
</td>
<td>
<p>Make the following changes at Kodi;</p>
<ol>
<li>Install the SickGear repo to access its Kodi Add-on
<ul>
<li>in <b class="boldest">Filemanager</b>, add a source for SickGear with <span class="grey-text">&lt;ip&gt;:&lt;port&gt;/kodi/</span> (e.g. <span class="grey-text">192.168.0.10:$sg_port/kodi/</span>)<br>
and name it for example, <span class="grey-text">SickGear</span>. <em>You will need to allow <span class="highlight-text">Unknown Sources</span> if not already</em> </li>
<li>in <b class="boldest">System/Add-ons</b>, "<span class="grey-text">Install from zip file</span>", in the folder list, select the <span class="grey-text">SickGear</span> source</li>
<li>select the <span class="grey-text">repository.sickgear</span> in the folder listing, and install the repository zip<br>
<em>Kodi will connect to the SickGear app to download and install its Add-on repository</em></li>
</ul>
<li>Install the SickGear Add-on from the repo</li>
<ul>
<li>in <b class="boldest">System/Add-ons</b>, "<span class="grey-text">Install from zip repository</span>", select "<span class="grey-text">SickGear Add-on repository</span>" / "<span class="grey-text">Services</span>"<br>
<li>select Add-on "<span class="grey-text">SickGear Watched State Updater</span>"</li>
<li>configure Add-on and restart Kodi after install or after switching profiles for the first time</li>
</ul>
</ol>
</td>
</tr>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/emby.png"><span class="vmid">Emby</span>
<p>Episode watch states are periodically fetched and shown above.</p>
</td>
<td>
<ol>
<li>Enable Emby Media Server in <b class="boldest">config/Notifications</b></li>
<li>Choose an interval for updating watched states</li>
</ol>
</td>
</tr>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/plex.png"><span class="vmid">Plex</span>
<p>Episode watch states are periodically fetched and shown above.</p>
</td>
<td>
<ol>
<li>Enable Plex Media Server in <b class="boldest">config/Notifications</b></li>
<li>Choose an interval for updating watched states</li>
</ol>
</td>
</tr>
##
##
#elif 'stats' in $layout
##
##
#set sum = 0
#for $hItem in $stat_results
#set $sum += $hItem['count']
#end for
##
#if 'graph' in $layout
<tbody>
<tr><td>
#set $labels = []
#set $perc = []
#for $hItem in $stat_results
#set $p = (float($hItem['count']) / float($sum)) * 100
#if 1 <= $p:
#set $labels += [$hItem['provider']]
#set $perc += ['%s' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str($p))]
#end if
#end for
<script src="$sbRoot/js/plot.ly/plotly-latest.min.js?v=$sbPID"></script>
<script src="$sbRoot/js/plot.ly/numeric/1.2.6/numeric.min.js?v=$sbPID"></script>
<div id="plot-canvas" style="margin:15px auto 15px;width:550px;height:350px"></div>
<style>
.modebar-btn[data-title*="edit plot"]{display:none !important}
</style>
<script>
Plotly.newPlot('plot-canvas', [
{
values: [#echo ', '.join($perc)#],
labels: [#echo '\'%s\'' % '\', \''.join($labels)#],
name: 'SickGear provider activity',
hoverinfo: 'label+percent+name',
domain: {x: [0, .5]},
hole: .42,
type: 'pie'
}
],
{title: 'SickGear provider activity (1% and above for #echo ('latest %s' % $limit, 'all')['0' == $limit]#)',
annotations: [{font: {size: 14}, showarrow: false, text: 'Activity', x: 0.16, y: 0.5}]
}, {displaylogo: !1, showLink: !1});
</script>
</td></tr>
##
##
#else
##
##
<thead>
<tr>
<th>Provider</th>
<th>Activity Hits</th>
<th>Activity %</th>
<th>Latest Activity</th>
</tr>
</thead>
<tfoot>
<tr>
<th class="text-nowrap" colspan="4">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $stat_results
<tr>
<td class="provider text-nowrap">
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider']))
#if None is not $provider
<img src="$sbRoot/images/providers/<%= provider.image_name() %>" width="16" height="16"><span data-sort="$hItem['provider']">$provider.name</span>
#else
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" title="missing provider"><span data-sort="$hItem['provider']">Missing Provider</span>
#end if
</td>
<td>$hItem['count']</td>
<td>#echo '%s%%' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str((float($hItem['count'])/float($sum))*100))#</td>
#set $curdatetime = $datetime.datetime.strptime(str($hItem['latest']), $history.dateFormat)
<td><div class="${fuzzydate}" data-sort="$hItem['latest']">$sbdatetime.sbdatetime.sbfdatetime($curdatetime)</div></td>
</tr>
#end for
#end if
##
##
#elif 'failures' in $layout
##
##
<div id="provider-failures">
#if not $provider_fails
<p>No current failures. Failure stats display here when appropriate.</p>
#else
<p>When a provider cannot be contacted over a period, SickGear backs off and waits an increasing interval between each retry</p>
#for $prov in $provider_fail_stats
#if $len($prov['fails'])
<!-- $prov['name'] -->
<div>
#set $prov_class = '<span %sstyle="vertical-align:middle">'
#if not $prov['active']
#set $prov_class = $prov_class % 'class="grey-text" '
#else
#set $prov_class = $prov_class % ''
#end if
<input type="button" class="shows-more btn" value="Expand" style="display:none"><input type="button" class="shows-less btn" value="Collapse"><img src="$sbRoot/images/providers/$prov['prov_img']" width="16" height="16" style="margin:0 6px 0 3px">$prov_class$prov['name']
#if $prov['active']
#if $prov['next_try']
#set nt = $str($prov['next_try']).split('.', 2)[0][::-1].replace(':', ' m', 1).replace(':', ' h', 1)[::-1]
... is paused until $sbdatetime.sbdatetime.sbftime($sbdatetime.sbdatetime.now() + $prov['next_try'], markup=True) (in ${nt}s) <input type="button" class="provider-retry btn" id="$prov['prov_id']-btn-retry" value="Ignore pause on next search">
#end if
#else
... is not enabled
#end if
</span>
</div>
<table class="manageTable provider-failures tablesorter hover-highlight focus-highlight text-center" cellspacing="0" border="0" cellpadding="0">
<thead>
<tr>
<th class="text-center" style="width:13em;padding-right:20px">period of 1hr</th>
<th class="text-center" style="padding-right:20px">server/timeout</th>
<th class="text-center" style="padding-right:20px">network</th>
<th class="text-center" style="padding-right:20px">no data</th>
<th class="text-center" style="padding-right:20px">other</th>
#if $prov['has_limit']
<th class="text-center" style="padding-right:20px">hit limit</th>
#end if
</tr>
</thead>
<tbody>
#set $day = []
#for $fail in $prov['fails']
#set $child = True
#if $fail['date'] not in $day
#set $day += [$fail['date']]
#set $child = False
#end if
#slurp#
<tr#if $fail['multirow'] and $child# class="tablesorter-childRow"#end if#>
#if $fail['multirow']
#if not $child
<td><a href="#" class="provider-fail-parent-toggle" title="Totals (expand for detail)">$sbdatetime.sbdatetime.sbfdate($fail['date_time'])</a></td>
#else
<td>$sbdatetime.sbdatetime.sbftime($fail['date_time'], markup=True)</td>
#end if
#else
<td>$sbdatetime.sbdatetime.sbfdatetime($fail['date_time'], markup=True)</td>
#end if
#set $blank = '-'
#set $title=None
#if $fail['http']['count']
#set $title=$fail['http']['code']
#end if
<td>#if $fail['http']['count']#<span title="#if $child or not $fail['multirow']#$title#else#Expand for fail codes#end if#">$fail['http']['count']</span>#else#$blank#end if# / #echo $fail['timeout'].get('count', 0) or $blank#</td>
<td>#echo ($fail['connection'].get('count', 0) + $fail['connection_timeout'].get('count', 0)) or $blank#</td>
<td>#echo $fail['nodata'].get('count', 0) or $blank#</td>
<td>#echo $fail['other'].get('count', 0) or $blank#</td>
#if $prov['has_limit']
<td>#echo $fail.get('limit', {}).get('count', 0) or $blank#</td>
#end if
</tr>
#end for
</tbody>
</table>
<!-- /$prov['name'] -->
#end if
#end for
#end if
</div>
##
##
#end if
#if 'failure' not in $layout
</tbody>
</table>
#end if
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -1,6 +1,7 @@
#import sickbeard
#import datetime
#from sickbeard import common
#from sickbeard.common import (Overview, statusStrings, ARCHIVED, FAILED, IGNORED, SKIPPED,
SNATCHED, SNATCHED_ANY, UNKNOWN, WANTED, DOWNLOADED)
##
#set global $title = 'Episode Overview'
#set global $header = 'Episode Overview'
@ -19,7 +20,7 @@
#if not $whichStatus or ($whichStatus and not $ep_counts)
##
#if $whichStatus:
<h3>no episodes have status <span class="grey-text">$common.statusStrings[$whichStatus].lower()</span></h3>
<h3>no episodes have status <span class="grey-text">$statusStrings[$whichStatus].lower()</span></h3>
#end if
<form action="$sbRoot/manage/episodeStatuses" method="get">
@ -27,8 +28,8 @@
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, $common.DOWNLOADED]:
<option value="$curStatus"#echo ('', ' selected="selected"')[$curStatus == $default_manage]#>$common.statusStrings[$curStatus]</option>
#for $curStatus in [$SKIPPED, $UNKNOWN, $SNATCHED, $WANTED, $ARCHIVED, $IGNORED, $DOWNLOADED]:
<option value="$curStatus"#echo ('', ' selected="selected"')[$curStatus == $default_manage]#>$statusStrings[$curStatus]</option>
#end for
</select>
@ -36,26 +37,26 @@
</form>
##
#else
#if $whichStatus in ($common.ARCHIVED, $common.IGNORED):
#if $whichStatus in ($ARCHIVED, $IGNORED):
#set $row_class = 'good'
#elif $whichStatus == $common.SNATCHED:
#elif $whichStatus == $SNATCHED:
#set $row_class = 'snatched'
#else
#set $row_class = $common.Overview.overviewStrings[$whichStatus]
#set $row_class = $Overview.overviewStrings[$whichStatus]
#end if
#set $statusList = [$common.SKIPPED, $common.ARCHIVED, $common.IGNORED]
#if $common.DOWNLOADED == $whichStatus:
#set $statusList = [$common.ARCHIVED]
#elif $common.ARCHIVED == $whichStatus:
#set $statusList = [$common.SKIPPED, $common.DOWNLOADED, $common.ARCHIVED, $common.IGNORED]
#set $statusList = [$ARCHIVED, $IGNORED, $SKIPPED]
#if $DOWNLOADED == $whichStatus:
#set $statusList = [$ARCHIVED]
#elif $ARCHIVED == $whichStatus:
#set $statusList = [$SKIPPED, $DOWNLOADED, $ARCHIVED, $IGNORED]
#end if
#if $whichStatus in $statusList
$statusList.remove($whichStatus)
#end if
#if $whichStatus in $common.SNATCHED_ANY
$statusList.append($common.FAILED)
#if $whichStatus in $SNATCHED_ANY
$statusList.append($FAILED)
#end if
<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?v=$sbPID"></script>
@ -63,7 +64,7 @@
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus">
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$common.statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>
<input type="hidden" id="row_class" value="$row_class">
@ -71,16 +72,16 @@
<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>
<option value="$curStatus">$statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline go" type="submit" value="Go">
#if $common.DOWNLOADED != $whichStatus:
#if $DOWNLOADED != $whichStatus:
<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>
<option value="$UNKNOWN">nothing</option>
<option value="$WANTED">$statusStrings[$WANTED]</option>
</select>
<input class="btn btn-inline go" type="submit" value="Go">
#end if

View file

@ -63,83 +63,6 @@
<div id="provider-failures" class="section">
<h3>Provider Failures:</h3>
#if not $provider_fails
<p>No current failures. Failure stats display here when appropriate.</p>
#else
<p>Some providers can be often down over periods, SickGear will back off then retry connecting at a later time</p>
#for $prov in $provider_fail_stats
#if $len($prov['fails'])
<!-- $prov['name'] -->
<div>
<input type="button" class="shows-more btn" value="Expand" style="display:none"><input type="button" class="shows-less btn" value="Collapse"><img src="$sbRoot/images/providers/$prov['prov_img']" width="16" height="16" style="margin:0 6px 0 3px">$prov['name']
#if $prov['active']
#if $prov['next_try']
#set nt = $str($prov['next_try']).split('.', 2)
... is blocked until $sbdatetime.sbdatetime.sbftime($sbdatetime.sbdatetime.now() + $prov['next_try'], markup=True) (in $nt[0]) <input type="button" class="provider-retry btn" id="$prov['prov_id']-btn-retry" value="Ignore block on next search">
#end if
#else
... is not enabled
#end if
</div>
<table class="manageTable provider-failures tablesorter hover-highlight focus-highlight text-center" cellspacing="0" border="0" cellpadding="0">
<thead>
<tr>
<th class="text-center" style="width:13em;padding-right:20px">period of 1hr</th>
<th class="text-center" style="padding-right:20px">server/timeout</th>
<th class="text-center" style="padding-right:20px">network</th>
<th class="text-center" style="padding-right:20px">no data</th>
<th class="text-center" style="padding-right:20px">other</th>
#if $prov['has_limit']
<th class="text-center" style="padding-right:20px">hit limit</th>
#end if
</tr>
</thead>
<tbody>
#set $day = []
#for $fail in $prov['fails']
#set $child = True
#if $fail['date'] not in $day
#set $day += [$fail['date']]
#set $child = False
#end if
#slurp#
<tr#if $fail['multirow'] and $child# class="tablesorter-childRow"#end if#>
#if $fail['multirow']
#if not $child
<td><a href="#" class="provider-fail-parent-toggle" title="Totals (expand for detail)">$sbdatetime.sbdatetime.sbfdate($fail['date_time'])</a></td>
#else
<td>$sbdatetime.sbdatetime.sbftime($fail['date_time'], markup=True)</td>
#end if
#else
<td>$sbdatetime.sbdatetime.sbfdatetime($fail['date_time'], markup=True)</td>
#end if
#set $blank = '-'
#set $title=None
#if $fail['http']['count']
#set $title=$fail['http']['code']
#end if
<td>#if $fail['http']['count']#<span title="#if $child or not $fail['multirow']#$title#else#Expand for fail codes#end if#">$fail['http']['count']</span>#else#$blank#end if# / #echo $fail['timeout'].get('count', 0) or $blank#</td>
<td>#echo ($fail['connection'].get('count', 0) + $fail['connection_timeout'].get('count', 0)) or $blank#</td>
<td>#echo $fail['nodata'].get('count', 0) or $blank#</td>
<td>#echo $fail['other'].get('count', 0) or $blank#</td>
#if $prov['has_limit']
<td>#echo $fail.get('limit', {}).get('count', 0) or $blank#</td>
#end if
</tr>
#end for
</tbody>
</table>
<!-- /$prov['name'] -->
#end if
#end for
#end if
</div>
<div id="search-queues" class="section">
<h3>Search Queues:</h3>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>Index of $basepath</h1>
<table border="1" cellpadding="5" cellspacing="0" class="whitelinks">
<tr>
<th>Name</th>
</tr>
#for $file in $filelist
<tr>
<td><a href="$file">$file</a></td>
</tr>
#end for
</table>
<hr>
<em>Tornado Server for SickGear</em>
</body>
</html>

View file

@ -0,0 +1,27 @@
##
#from sickbeard import WEB_PORT, WEB_ROOT, ENABLE_HTTPS
#set sg_host = $getVar('sbHost', 'localhost')
#set sg_port = str($getVar('sbHttpPort', WEB_PORT))
#set sg_root = $getVar('sbRoot', WEB_ROOT)
#set sg_use_https = $getVar('sbHttpsEnabled', ENABLE_HTTPS)
##
#set $base_url = 'http%s://%s:%s%s' % (('', 's')[any([sg_use_https])], $sg_host, $sg_port, $sg_root)
##
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="repository.sickgear" name="SickGear Add-on repository" version="1.0.0" provider-name="SickGear">
<extension point="xbmc.addon.repository"
name="SickGear Add-on Repository">
<info compressed="true">$base_url/kodi/addons.xml</info>
<checksum>$base_url/kodi/addons.xml.md5</checksum>
<datadir zip="true">$base_url/kodi</datadir>
<hashes>false</hashes>
</extension>
<extension point="xbmc.addon.metadata">
<summary>Install Add-ons for SickGear</summary>
<description>Download and install add-ons from a repository at a running SickGear instance.[CR][CR]Contains:[CR]* Watchedstate updater service</description>
<disclaimer></disclaimer>
<platform>all</platform>
<website>https://github.com/SickGear/SickGear</website>
<nofanart>true</nofanart>
</extension>
</addon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addons>
$watchedstate_updater_addon_xml
$repo_xml
</addons>

View file

@ -10,7 +10,7 @@
var _disable_empty_list=false;
var _hide_empty_list=false;
function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){
function goListGroup(apikey, L8, L7, L6, L5, L4, L3, L2, L1){
var GlobalOptions = "";
$('.global').each(function(){
var checked = $(this).prop('checked');
@ -26,7 +26,7 @@ function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){
});
// handle the show.getposter / show.getbanner differently as they return an image and not json
if (L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner") {
if (L1 == "?cmd=sg.getnetworkicon" || L1 == "?cmd=sg.show.getposter" || L1 == "?cmd=sg.show.getbanner" || L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner" || L1 == "?cmd=sg.getindexericon") {
var imgcache = sbRoot + "/api/" + apikey + "/" + L1 + L2 + GlobalOptions;
var html = imgcache + '<br/><br/><img src="' + sbRoot + '/images/loading16.gif" id="imgcache">';
$('#apiResponse').html(html);
@ -36,14 +36,24 @@ function goListGroup(apikey, L7, L6, L5, L4, L3, L2, L1){
cache: false,
dataType: "html",
success: function (img) {
$('#imgcache').attr('src', imgcache);
$('#imgcache').attr('src', imgcache + "&random=" + Math.random() * 100000000000000000000);
}
})
}
else if (L1 == "?cmd=listcommands")
{
var html = $.ajax({
url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions,
async: false,
dataType: "html",
}).responseText;
$('#apiResponse').html(html);
}
else {
var html = sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + GlobalOptions + "<br/><pre>";
var html = sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions + "<br/><pre>";
html += $.ajax({
url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + GlobalOptions,
url: sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions,
async: false,
dataType: "html",
}).responseText;
@ -167,7 +177,7 @@ function cs_addL(dis,link,label,css) { this.items[this.items.length]=new cs_link
function cs_addG(label,css) { this.items[this.items.length]=new cs_groupOBJ(label,css); }
function cs_endG() { this.items[this.items.length]=new cs_groupOBJ2(); }
function cs_showMsg(msg) { window.status=msg; }
function cs_showMsg(msg) { console.error(msg); window.status=msg; }
function cs_badContent(n) { cs_goodContent=false; cs_showMsg("["+n+"] Not Found."); }
function _setCookie(name, value) {
@ -636,6 +646,6 @@ function selectOptions(n,opts,mode) {
}
}
}
}
}
}
// ------

View file

@ -1,6 +1,18 @@
function toggle$(el, cond){
var ifId = '-if-' + $(el).attr('id');
if(cond){
$('.hide' + ifId).fadeOut('fast', 'linear');
$('.show' + ifId).fadeIn('fast', 'linear');
} else {
$('.show' + ifId).fadeOut('fast', 'linear');
$('.hide' + ifId).fadeIn('fast', 'linear');
}
}
$(document).ready(function () {
var enabler = $('.enabler'),
viewIf = $('.viewIf');
viewIf = $('input.view-if'),
viewIfSel = $('select.view-if');
enabler.each(function () {
if (!$(this).prop('checked'))
@ -16,18 +28,19 @@ $(document).ready(function () {
});
viewIf.each(function () {
$(($(this).prop('checked') ? '.hide_if_' : '.show_if_') + $(this).attr('id')).hide();
$(($(this).prop('checked') ? '.hide-if-' : '.show-if-') + $(this).attr('id')).hide();
});
viewIf.click(function () {
var if_id = '_if_' + $(this).attr('id');
if ($(this).prop('checked')) {
$('.hide' + if_id).fadeOut('fast', 'linear');
$('.show' + if_id).fadeIn('fast', 'linear');
} else {
$('.show' + if_id).fadeOut('fast', 'linear');
$('.hide' + if_id).fadeIn('fast', 'linear');
}
toggle$(this, $(this).prop('checked'));
});
viewIfSel.each(function () {
$((0 < $(this).find('option:selected').val() ? '.hide-if-' : '.show-if-') + $(this).attr('id')).hide();
});
viewIfSel.change(function(){
toggle$(this, 0 < $(this).find('option:selected').val());
});
var idSelect = '#imdb-accounts', idDel = '#imdb-list-del', idInput = '#imdb-url', idOnOff = '#imdb-list-onoff',

View file

@ -68,12 +68,12 @@ $(document).ready(function () {
var showname = document.getElementById('showtitle').getAttribute('data-showname');
$.confirm({
'title' : 'Remove Show',
'message' : 'Are you sure you want to remove <span class="footerhighlight">' + showname + '</span> from the database ?<br /><br /><input type="checkbox" id="deleteFiles"> <span class="red-text">Check to delete files as well. IRREVERSIBLE</span></input>',
'message' : 'Are you sure you want to remove <span class="footerhighlight">' + showname + '</span> from the database ?<br /><br /><input type="checkbox" id="delete-files"> <span class="red-text">Check to delete files as well. IRREVERSIBLE</span>',
'buttons' : {
'Yes' : {
'class' : 'green',
'action': function(){
location.href = target + (document.getElementById('deleteFiles').checked ? '&full=1' : '');
location.href = target + (document.getElementById('delete-files').checked ? '&full=1' : '');
// If checkbox is ticked, remove show and delete files. Else just remove show.
}
},
@ -85,6 +85,98 @@ $(document).ready(function () {
});
});
$('#del-watched').bind('click', function(e) {
e.preventDefault();
var dedupe = [], delArr = [], mFiles = 0;
$('.del-check').each(function() {
if (!0 === this.checked) {
var pathFile = $(this).closest('tr').attr('data-file'),
thisId = $(this).attr('id');
if (-1 === jQuery.inArray(pathFile, dedupe)) {
dedupe.push(pathFile);
mFiles += 1 - $(this).closest('tr').find('.tvShow .strike-deleted').length;
}
delArr.push(thisId.replace('del-', ''));
/** @namespace $.SickGear.history.isCompact */
if ($.SickGear.history.isCompact) {
// then select all related episode checkboxes
var tvepId = $(this).closest('tr').attr('data-tvep-id');
$('tr[data-tvep-id="' + tvepId + '"] input.del-check:not("#' + thisId + '")')
.each(function(){
delArr.push($(this).attr('id').replace('del-', ''));
});
}
}
});
if (0 === delArr.length)
return !1;
/** @namespace $.SickGear.history.isTrashit */
/** @namespace $.SickGear.history.lastDeleteFiles */
/** @namespace $.SickGear.history.lastDeleteRecords */
var action = $.SickGear.history.isTrashit ? 'Trash' : 'Delete',
btns = {
'Yes' : {
'class' : 'green',
'action': function(){
var deleteFiles = !!$('#delete-files:checked').length,
deleteRecords = !!$('#delete-records:checked').length,
checked = ' checked="checked"';
$.SickGear.history.lastDeleteFiles = deleteFiles ? checked : '';
$.SickGear.history.lastDeleteRecords = deleteRecords ? checked : '';
$.post($.SickGear.Root + '/history/watched',
{
tvew_id: delArr.join('|'),
files: (deleteFiles ? '1' : ''),
records: (deleteRecords ? '1' : '')
},
function(data){
var result = $.parseJSON(data);
result.success && window.location.reload(true);
/* using window.location as the following is
sluggish when deleting 20 of 100 records
*/
/*
result.success && $.each(result.success, function(){
var tr = $('#del-' + this).closest('tr');
var t = tr.closest('table');
tr.addClass('delete-me').fadeToggle('fast', 'linear').promise().done(
function(){
$('.delete-me').html('');
t.trigger('update');
$.SickGear.sumChecked();
});
});*/
});
}
}
};
// btn pre-created here in order to use a custom btn text as named key to object
btns['No' + (0 < mFiles ? ', abort ' + ($.SickGear.history.isTrashit ? 'trash' : 'permanent delete') : '')] = {'class' : 'red'};
$.confirm({
'title' : (action + (0 < mFiles ? ' media' : ' records')
+ '<span style="float:right;font-size:12px">(<a class="highlight-text contrast-text" href="/config/general/">"Send to trash" options</a>)</span>'),
'message' : (0 < mFiles
? '<input id="delete-files" style="margin-right:6px"' + $.SickGear.history.lastDeleteFiles + ' type="checkbox">'
+ '<span>' + action + ' <span class="footerhighlight">' + mFiles + '</span>'
+ ' media file' + (1===mFiles?'':'s') + ' from disk</span>'
: ''
)
+ '<span style="display:block;margin-top:20px">'
+ '<input id="delete-records" style="margin-right:6px"' + $.SickGear.history.lastDeleteRecords + ' type="checkbox">'
+ 'Remove <span class="footerhighlight">'
+ delArr.length + '</span> history record' + (1===delArr.length?'':'s')
+ '</span>'
+ '<span class="red-text" style="display:block;margin-top:20px">Are you sure ?</span>',
'buttons' : btns
});
});
$('a.clearhistory').bind('click',function(e) {
e.preventDefault();
var target = $( this ).attr('href');
@ -105,7 +197,7 @@ $(document).ready(function () {
}
});
});
$('a.trimhistory').bind('click',function(e) {
e.preventDefault();
var target = $( this ).attr('href');
@ -126,5 +218,5 @@ $(document).ready(function () {
}
});
});
});
});

236
gui/slick/js/history.js Normal file
View file

@ -0,0 +1,236 @@
/** @namespace $.SickGear.Root */
/** @namespace $.SickGear.history.isCompact */
/** @namespace $.SickGear.history.isTrashit */
/** @namespace $.SickGear.history.useSubtitles */
/** @namespace $.SickGear.history.layoutName */
/*
2017 Jason Mulligan <jason.mulligan@avoidwork.com>
@version 3.5.11
*/
!function(i){function e(i){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[],d=0,r=void 0,a=void 0,s=void 0,f=void 0,u=void 0,l=void 0,v=void 0,B=void 0,c=void 0,p=void 0,y=void 0,m=void 0,x=void 0,g=void 0;if(isNaN(i))throw new Error("Invalid arguments");return s=!0===e.bits,y=!0===e.unix,a=e.base||2,p=void 0!==e.round?e.round:y?1:2,m=void 0!==e.spacer?e.spacer:y?"":" ",g=e.symbols||e.suffixes||{},x=2===a?e.standard||"jedec":"jedec",c=e.output||"string",u=!0===e.fullform,l=e.fullforms instanceof Array?e.fullforms:[],r=void 0!==e.exponent?e.exponent:-1,B=Number(i),v=B<0,f=a>2?1e3:1024,v&&(B=-B),(-1===r||isNaN(r))&&(r=Math.floor(Math.log(B)/Math.log(f)))<0&&(r=0),r>8&&(r=8),0===B?(n[0]=0,n[1]=y?"":t[x][s?"bits":"bytes"][r]):(d=B/(2===a?Math.pow(2,10*r):Math.pow(1e3,r)),s&&(d*=8)>=f&&r<8&&(d/=f,r++),n[0]=Number(d.toFixed(r>0?p:0)),n[1]=10===a&&1===r?s?"kb":"kB":t[x][s?"bits":"bytes"][r],y&&(n[1]="jedec"===x?n[1].charAt(0):r>0?n[1].replace(/B$/,""):n[1],o.test(n[1])&&(n[0]=Math.floor(n[0]),n[1]=""))),v&&(n[0]=-n[0]),n[1]=g[n[1]]||n[1],"array"===c?n:"exponent"===c?r:"object"===c?{value:n[0],suffix:n[1],symbol:n[1]}:(u&&(n[1]=l[r]?l[r]:b[x][r]+(s?"bit":"byte")+(1===n[0]?"":"s")),n.join(m))}var o=/^(b|B)$/,t={iec:{bits:["b","Kib","Mib","Gib","Tib","Pib","Eib","Zib","Yib"],bytes:["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"]},jedec:{bits:["b","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb"],bytes:["B","KB","MB","GB","TB","PB","EB","ZB","YB"]}},b={iec:["","kibi","mebi","gibi","tebi","pebi","exbi","zebi","yobi"],jedec:["","kilo","mega","giga","tera","peta","exa","zetta","yotta"]};e.partial=function(i){return function(o){return e(o,i)}},"undefined"!=typeof exports?module.exports=e:"function"==typeof define&&define.amd?define(function(){return e}):i.filesize=e}("undefined"!=typeof window?window:global);
function rowCount(){
var output$ = $('#row-count');
if(!output$.length)
return;
var tbody$ = $('#tbody'),
nRows = tbody$.find('tr').length,
compacted = tbody$.find('tr.hide').length,
compactedFiltered = tbody$.find('tr.filtered.hide').length,
filtered = tbody$.find('tr.filtered').length;
output$.text((filtered
? nRows - (filtered + compacted - compactedFiltered) + ' / ' + nRows + ' filtered'
: nRows) + (1 === nRows ? ' row' : ' rows'));
}
$(document).ready(function() {
var extraction = {0: function(node) {
var dataSort = $(node).find('div[data-sort]').attr('data-sort')
|| $(node).find('span[data-sort]').attr('data-sort');
return !dataSort ? dataSort : dataSort.toLowerCase();}},
tbody$ = $('#tbody'),
headers = {},
layoutName = '' + $.SickGear.history.layoutName;
if ('detailed' === layoutName) {
jQuery.extend(extraction, {
4: function (node) {
return $(node).find('span').text().toLowerCase();
}
});
jQuery.extend(headers, {4: {sorter: 'quality'}});
} else if ('compact' === layoutName) {
jQuery.extend(extraction, {
1: function (node) {
return $(node).find('span[data-sort]').attr('data-sort').toLowerCase();
},
2: function (node) {
return $(node).attr('provider').toLowerCase();
},
5: function (node) {
return $(node).attr('quality').toLowerCase();
}
});
var disable = {sorter: !1}, qualSort = {sorter: 'quality'};
jQuery.extend(headers, $.SickGear.history.useSubtitles ? {4: disable, 5: qualSort} : {3: disable, 4: qualSort});
} else if (-1 !== layoutName.indexOf('watched')) {
jQuery.extend(extraction, {
3: function(node) {
return $(node).find('span[data-sort]').attr('data-sort');
},
5: function(node) {
return $(node).find('span[data-sort]').attr('data-sort');
},
6: function (node) {
return $(node).find('input:checked').length;
}
});
jQuery.extend(headers, {4: {sorter: 'quality'}});
rowCount();
} else if (-1 !== layoutName.indexOf('compact_stats')) {
jQuery.extend(extraction, {
3: function (node) {
return $(node).find('div[data-sort]').attr('data-sort');
}
});
}
var isWatched = -1 !== $('select[name="HistoryLayout"]').val().indexOf('watched'),
options = {
widgets: ['zebra', 'filter'],
widgetOptions : {
filter_hideEmpty: !0, filter_matchType : {'input': 'match', 'select': 'match'},
filter_resetOnEsc: !0, filter_saveFilters: !0, filter_searchDelay: 300
},
sortList: isWatched ? [[1, 1], [0, 1]] : [0, 1],
textExtraction: extraction,
headers: headers},
stateLayoutDate = function(table$, glyph$){table$.toggleClass('event-age');glyph$.toggleClass('age date');};
if(isWatched){
jQuery.extend(options, {
selectorSort: '.tablesorter-header-inside',
headerTemplate: '<div class="tablesorter-header-inside" style="margin:0 -8px 0 -4px">{content}{icon}</div>',
onRenderTemplate: function(index, template){
if(0 === index){
template = '<i id="watched-date" class="icon-glyph date add-qtip" title="Change date layout" style="float:left;margin:4px -14px 0 2px"></i>'
+ template;
}
return template;
},
onRenderHeader: function(){
var table$ = $('#history-table'), glyph$ = $('#watched-date');
if($.tablesorter.storage(table$, 'isLayoutAge')){
stateLayoutDate(table$, glyph$);
}
$(this).find('#watched-date').on('click', function(){
stateLayoutDate(table$, glyph$);
$.tablesorter.storage(table$, 'isLayoutAge', table$.hasClass('event-age'));
return !1;
});
}
});
}
$('#history-table').tablesorter(options).bind('filterEnd', function(){
rowCount();
});
$('#limit').change(function(){
window.location.href = $.SickGear.Root + '/history/?limit=' + $(this).val()
});
$('#show-watched-help').click(function () {
$('#watched-help').fadeToggle('fast', 'linear');
$.get($.SickGear.Root + '/history/toggle_help');
});
var addQTip = (function(){
$(this).css('cursor', 'help');
$(this).qtip({
show: {solo:true},
position: {viewport:$(window), my:'left center', adjust:{y: -10, x: 2}},
style: {tip: {corner:true, method:'polygon'}, classes:'qtip-dark qtip-rounded qtip-shadow'}
});
});
$('.add-qtip').each(addQTip);
$.SickGear.sumChecked = (function(){
var dedupe = [], sum = 0, output;
$('.del-check:checked').each(function(){
if ($(this).closest('tr').find('.tvShow .strike-deleted').length)
return;
var pathFile = $(this).closest('tr').attr('data-file');
if (-1 === jQuery.inArray(pathFile, dedupe)) {
dedupe.push(pathFile);
output = $(this).closest('td').prev('td.size').find('span[data-sort]').attr('data-sort');
sum = sum + parseInt(output, 10);
}
});
$('#del-watched').attr('disabled', !dedupe.length && !$('#tbody').find('tr').find('.tvShow .strike-deleted').length);
output = filesize(sum, {symbols: {B: 'Bytes'}});
$('#sum-size').text(/\s(MB)$/.test(output) ? filesize(sum, {round:1})
: /^1\sB/.test(output) ? output.replace('Bytes', 'Byte') : output);
});
$.SickGear.sumChecked();
var className='.del-check', lastCheck = null, check, found;
tbody$.on('click', className, function(ev){
if(!lastCheck || !ev.shiftKey){
lastCheck = this;
} else {
check = this; found = 0;
$('#tbody').find('> tr:visible').find(className).each(function(){
if (2 === found)
return !1;
if (1 === found)
this.checked = lastCheck.checked;
found += (1 && (this === check || this === lastCheck));
});
}
$(this).closest('table').trigger('update');
$.SickGear.sumChecked();
});
$('.shows-less').click(function(){
var table$ = $(this).nextAll('table:first');
table$ = table$.length ? table$ : $(this).parent().nextAll('table:first');
table$.hide();
$(this).hide();
$(this).prevAll('input:first').show();
});
$('.shows-more').click(function(){
var table$ = $(this).nextAll('table:first');
table$ = table$.length ? table$ : $(this).parent().nextAll('table:first');
table$.show();
$(this).hide();
$(this).nextAll('input:first').show();
});
$('.provider-retry').click(function () {
$(this).addClass('disabled');
var match = $(this).attr('id').match(/^(.+)-btn-retry$/);
$.ajax({
url: $.SickGear.Root + '/manage/manageSearches/retryProvider?provider=' + match[1],
type: 'GET',
complete: function () {
window.location.reload(true);
}
});
});
$('.provider-failures').tablesorter({widgets : ['zebra'],
headers : { 0:{sorter:!1}, 1:{sorter:!1}, 2:{sorter:!1}, 3:{sorter:!1}, 4:{sorter:!1}, 5:{sorter:!1} }
});
$('.provider-fail-parent-toggle').click(function(){
$(this).closest('tr').nextUntil('tr:not(.tablesorter-childRow)').find('td').toggle();
return !1;
});
// Make table cell focusable
// http://css-tricks.com/simple-css-row-column-highlighting/
var focus$ = $('.focus-highlight');
if (focus$.length){
focus$.find('td, th')
.attr('tabindex', '1')
// add touch device support
.on('touchstart', function(){
$(this).focus();
});
}
});

View file

@ -34,37 +34,4 @@ $(function(){
$(this).hide();
$(this).nextAll('input:first').show();
});
$('.provider-retry').click(function () {
$(this).addClass('disabled');
var match = $(this).attr('id').match(/^(.+)-btn-retry$/);
$.ajax({
url: sbRoot + '/manage/manageSearches/retryProvider?provider=' + match[1],
type: 'GET',
complete: function () {
window.location.reload(true);
}
});
});
$('.provider-failures').tablesorter({widgets : ['zebra'],
headers : { 0:{sorter:!1}, 1:{sorter:!1}, 2:{sorter:!1}, 3:{sorter:!1}, 4:{sorter:!1}, 5:{sorter:!1} }
});
$('.provider-fail-parent-toggle').click(function(){
$(this).closest('tr').nextUntil('tr:not(.tablesorter-childRow)').find('td').toggle();
return !1;
});
// Make table cell focusable
// http://css-tricks.com/simple-css-row-column-highlighting/
var focus$ = $('.focus-highlight');
if (focus$.length){
focus$.find('td, th')
.attr('tabindex', '1')
// add touch device support
.on('touchstart', function(){
$(this).focus();
});
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
lib/plex/__init__.py Normal file
View file

@ -0,0 +1 @@
from plex import *

423
lib/plex/plex.py Normal file
View file

@ -0,0 +1,423 @@
# -*- coding: utf-8 -*-
#
# 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/>.
from time import sleep
import datetime
import math
import os
import platform
import re
import sys
try:
from urllib import urlencode # Python2
except ImportError:
import urllib
from urllib.parse import urlencode # Python3
try:
import urllib.request as urllib2
except ImportError:
import urllib2
from sickbeard import logger
from sickbeard.helpers import getURL, tryInt
try:
from lxml import etree
except ImportError:
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
class Plex:
def __init__(self, settings=None):
settings = settings or {}
self._plex_host = settings.get('plex_host') or '127.0.0.1'
self.plex_port = settings.get('plex_port') or '32400'
self.username = settings.get('username', '')
self.password = settings.get('password', '')
self.token = settings.get('token', '')
self.device_name = settings.get('device_name', '')
self.client_id = settings.get('client_id') or '5369636B47656172'
self.machine_client_identifier = ''
self.default_home_users = settings.get('default_home_users', '')
# Progress percentage to consider video as watched
# if set to anything > 0, videos with watch progress greater than this will be considered watched
self.default_progress_as_watched = settings.get('default_progress_as_watched', 0)
# Sections to scan. If empty all sections will be looked at,
# the section id should be used which is the number found be in the url on PlexWeb after /section/[ID]
self.section_list = settings.get('section_list', [])
# Sections to skip scanning, for use when Settings['section_list'] is not specified,
# the same as section_list, the section id should be used
self.ignore_sections = settings.get('ignore_sections', [])
# Filter sections by paths that are in this array
self.section_filter_path = settings.get('section_filter_path', [])
# Results
self.show_states = {}
self.file_count = 0
# Conf
self.config_version = 2.0
self.use_logger = False
self.test = None
self.home_user_tokens = {}
if self.username and '' == self.token:
self.token = self.get_token(self.username, self.password)
@property
def plex_host(self):
if not self._plex_host.startswith('http'):
return 'http://%s' % self.plex_host
return self._plex_host
@plex_host.setter
def plex_host(self, value):
self._plex_host = value
def log(self, msg, debug=True):
try:
if self.use_logger:
msg = 'Plex:: ' + msg
if debug:
logger.log(msg, logger.DEBUG)
else:
logger.log(msg)
# else:
# print(msg.encode('ascii', 'replace').decode())
except (StandardError, Exception):
pass
def get_token(self, user, passw):
auth = ''
try:
auth = getURL('https://plex.tv/users/sign_in.json',
headers={'X-Plex-Device-Name': 'SickGear',
'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
'X-Plex-Platform-Version': platform.release(),
'X-Plex-Provides': 'Python', 'X-Plex-Product': 'Python',
'X-Plex-Client-Identifier': self.client_id,
'X-Plex-Version': str(self.config_version),
'X-Plex-Username': user
},
json=True,
data=urlencode({b'user[login]': user, b'user[password]': passw}).encode('utf-8')
)['user']['authentication_token']
except IndexError:
self.log('Error getting Plex Token')
return auth
def get_access_token(self, token):
resources = self.get_url_x('https://plex.tv/api/resources?includeHttps=1', token=token)
if None is resources:
return ''
devices = resources.findall('Device')
for device in devices:
if 1 == len(devices) \
or self.machine_client_identifier == device.get('clientIdentifier') \
or (self.device_name
and (self.device_name.lower() in device.get('name').lower()
or self.device_name.lower() in device.get('clientIdentifier').lower())
):
access_token = device.get('accessToken')
if not access_token:
return ''
return access_token
connections = device.findall('Connection')
for connection in connections:
if self.plex_host == connection.get('address'):
access_token = device.get('accessToken')
if not access_token:
return ''
uri = connection.get('uri')
match = re.compile('(http[s]?://.*?):(\d*)').match(uri)
if match:
self.plex_host = match.group(1)
self.plex_port = match.group(2)
return access_token
return ''
def get_plex_home_user_tokens(self):
user_tokens = {}
# check Plex is contactable
home_users = self.get_url_x('https://plex.tv/api/home/users')
if None is not home_users:
for user in home_users.findall('User'):
user_id = user.get('id')
# use empty byte data to force POST
switch_page = self.get_url_x('https://plex.tv/api/home/users/%s/switch' % user_id, data=b'')
if None is not switch_page:
home_token = 'user' == switch_page.tag and switch_page.get('authenticationToken')
if home_token:
username = switch_page.get('title')
user_tokens[username] = self.get_access_token(home_token)
return user_tokens
def get_url_x(self, url, token=None, **kwargs):
if not token:
token = self.token
if not url.startswith('http'):
url = 'http://' + url
for x in range(0, 3):
if 0 < x:
sleep(0.5)
try:
headers = {'X-Plex-Device-Name': 'SickGear',
'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
'X-Plex-Platform-Version': platform.release(),
'X-Plex-Provides': 'controller', 'X-Plex-Product': 'Python',
'X-Plex-Client-Identifier': self.client_id,
'X-Plex-Version': str(self.config_version),
'X-Plex-Token': token,
'Accept': 'application/xml'
}
if self.username:
headers.update({'X-Plex-Username': self.username})
page = getURL(url, headers=headers, **kwargs)
if page:
parsed = etree.fromstring(page)
if None is not parsed and len(parsed):
return parsed
return None
except Exception as e:
self.log('Error requesting page: %s' % e)
continue
return None
# uses the Plex API to delete files instead of system functions, useful for remote installations
def delete_file(self, media_id=0):
try:
endpoint = ('/library/metadata/%s' % str(media_id))
req = urllib2.Request('%s:%s%s' % (self.plex_host, self.plex_port, endpoint),
None, {'X-Plex-Token': self.token})
req.get_method = lambda: 'DELETE'
urllib2.urlopen(req)
except (StandardError, Exception):
return False
return True
@staticmethod
def get_media_info(video_node):
progress = 0
if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
progress = tryInt(video_node.get('viewOffset')) * 100 / tryInt(video_node.get('duration'))
for media in video_node.findall('Media'):
for part in media.findall('Part'):
file_name = part.get('file')
# if '3' > sys.version: # remove HTML quoted characters, only works in python < 3
# file_name = urllib2.unquote(file_name.encode('utf-8', errors='replace'))
# else:
file_name = urllib2.unquote(file_name)
return {'path_file': file_name, 'media_id': video_node.get('ratingKey'),
'played': int(video_node.get('viewCount') or 0), 'progress': progress}
def check_users_watched(self, users, media_id):
if not self.home_user_tokens:
self.home_user_tokens = self.get_plex_home_user_tokens()
result = {}
if 'all' in users:
users = self.home_user_tokens.keys()
for user in users:
user_media_page = self.get_url_pms('/library/metadata/%s' % media_id, token=self.home_user_tokens[user])
if None is not user_media_page:
video_node = user_media_page.find('Video')
progress = 0
if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
progress = tryInt(video_node.get('viewOffset')) * 100 / tryInt(video_node.get('duration'))
played = int(video_node.get('viewCount') or 0)
if not progress and not played:
continue
date_watched = 0
if (0 < tryInt(video_node.get('viewCount'))) or (0 < self.default_progress_as_watched < progress):
last_viewed_at = video_node.get('lastViewedAt')
if last_viewed_at and last_viewed_at not in ('', '0'):
date_watched = last_viewed_at
if date_watched:
result[user] = dict(played=played, progress=progress, date_watched=date_watched)
else:
self.log('Do not have the token for %s.' % user)
return result
def get_url_pms(self, endpoint=None, **kwargs):
return endpoint and self.get_url_x(
'%s:%s%s' % (self.plex_host, self.plex_port, endpoint), **kwargs)
# parse episode information from season pages
def stat_show(self, node):
episodes = []
if 'directory' == node.tag.lower() and 'show' == node.get('type'):
show = self.get_url_pms(node.get('key'))
if None is show: # Check if show page is None or empty
self.log('Failed to load show page. Skipping...')
return None
for season_node in show.findall('Directory'): # Each directory is a season
if 'season' != season_node.get('type'): # skips Specials
continue
season_key = season_node.get('key')
season = self.get_url_pms(season_key)
if None is not season:
episodes += [season]
elif 'mediacontainer' == node.tag.lower() and 'episode' == node.get('viewGroup'):
episodes = [node]
check_users = []
if self.default_home_users:
check_users = self.default_home_users.strip(' ,').lower().split(',')
for k in range(0, len(check_users)): # Remove extra spaces and commas
check_users[k] = check_users[k].strip(', ')
for episode_node in episodes:
for video_node in episode_node.findall('Video'):
media_info = self.get_media_info(video_node)
if check_users:
user_info = self.check_users_watched(check_users, media_info['media_id'])
for user_name, user_media_info in user_info.items():
self.show_states.update({len(self.show_states): dict(
path_file=media_info['path_file'],
media_id=media_info['media_id'],
played=(100 * user_media_info['played']) or user_media_info['progress'] or 0,
label=user_name,
date_watched=user_media_info['date_watched'])})
else:
self.show_states.update({len(self.show_states): dict(
path_file=media_info['path_file'],
media_id=media_info['media_id'],
played=(100 * media_info['played']) or media_info['progress'] or 0,
label=self.username,
date_watched=video_node.get('lastViewedAt'))})
self.file_count += 1
return True
def fetch_show_states(self, fetch_all=False):
error_log = []
self.show_states = {}
server_check = self.get_url_pms('/')
if None is server_check or 'MediaContainer' != server_check.tag:
error_log.append('Cannot reach server!')
else:
if not self.device_name:
self.device_name = server_check.get('friendlyName')
if not self.machine_client_identifier:
self.machine_client_identifier = server_check.get('machineIdentifier')
access_token = None
if self.token:
access_token = self.get_access_token(self.token)
if access_token:
self.token = access_token
if not self.home_user_tokens:
self.home_user_tokens = self.get_plex_home_user_tokens()
else:
error_log.append('Access Token not found')
resp_sections = None
if None is access_token or len(access_token):
resp_sections = self.get_url_pms('/library/sections/')
if None is not resp_sections:
unpather = []
for loc in self.section_filter_path:
loc = re.sub(r'[/\\]+', '/', loc.lower())
loc = re.sub(r'^(.{,2})[/\\]', '', loc)
unpather.append(loc)
self.section_filter_path = unpather
for section in resp_sections.findall('Directory'):
if 'show' != section.get('type') or not section.findall('Location'):
continue
section_path = re.sub(r'[/\\]+', '/', section.find('Location').get('path').lower())
section_path = re.sub(r'^(.{,2})[/\\]', '', section_path)
if not any([section_path in path for path in self.section_filter_path]):
continue
if section.get('key') not in self.ignore_sections \
and section.get('title') not in self.ignore_sections:
section_key = section.get('key')
for (user, token) in (self.home_user_tokens or {'': None}).iteritems():
self.username = user
resp_section = self.get_url_pms('/library/sections/%s/%s' % (
section_key, ('recentlyViewed', 'all')[fetch_all]), token=token)
if None is not resp_section:
view_group = 'MediaContainer' == resp_section.tag and \
resp_section.get('viewGroup') or ''
if 'show' == view_group and fetch_all:
for DirectoryNode in resp_section.findall('Directory'):
self.stat_show(DirectoryNode)
elif 'episode' == view_group and not fetch_all:
self.stat_show(resp_section)
if 0 < len(error_log):
self.log('Library errors...')
for item in error_log:
self.log(item)
return 0 < len(error_log)

103
readme.md
View file

@ -1,57 +1,76 @@
<hr>
<div><a id="top"><img alt="SickGear" width="200" src="https://raw.githubusercontent.com/wiki/SickGear/SickGear.Wiki/images/SickGearLogo.png"></a></div>
**SickGear**, a usenet and bittorrent PVR
*SickGear*, a usenet and bittorrent PVR
<hr>
_Please note you should know how to use git and setup basic requirements in order to run this software._
SickGear provides management of TV shows and/or Anime, it can detect new episodes, link to downloader apps, and more. SickGear is a proud descendant of Sick Beard and is humbled to have been endorsed by one of its former lead developers.
SickGear provides management of TV shows and/or Anime, it detects new episodes, links downloader apps, and more.
Why SickGear?
* SickGear maintains perfect uptime with the longest track record of being stable, reliable and trusted to work
* SickGear delivers quality from active development with a wealth of options on a dark or light themed interface
#### Why SickGear?
* SickGear maintains a perfect uptime with the longest track record of being stable, reliable and trusted to work
* SickGear delivers valued quality from active development with a wealth of options on a dark or light themed interface
#### What now?
* [Install guides](https://github.com/SickGear/SickGear/wiki/Installation-Instructions) for many platforms
* [Migrating](https://github.com/SickGear/SickGear/wiki/Install-SickGear-%5B0%5D-Migrate) to a hassle free and feature rich set up is super simple
## Features include
Or read more below...
## Some innovative SickGear features
* Ideas of shows to add from Trakt, IMDb, and AniDB categories; anticipating, new seasons, new shows, popular, and more
* Advanced add show finder that near always returns results sorted A-Z, Aired, or Relevancy, with known shows separated
* Releases can be searched during adding a show (e.g. optionally fetch oldest and/or newest without further effort)
* Advanced automated search that always works to prevent you wasting time manually scanning result lists
* Choose to delete watched episodes from a list built directly from played media at Kodi, Emby, and/or Plex (No Trakt!)
* Smart custom qualities selector system that helps achieve an optimal quality selection for automated episode search
* Choose to have episodes upgraded in quality, or keep existing archive quality, and upgrade future episodes either way
* Natively use a most powerful regex pattern matching system for superior information handling
* Select a UI style anytime; Regular, Proview I, or Proview II - independently for Episode View, and for Display Show
* Smart fanart system allows you to rate avoid/prefer. UI can be moved or toggled off/on to fully appreciate a fanart
* Episode View Layout "Day by Day" displays a fanart background from randomly selected imminent releases
* Configure from 0 to 500 fanart images to cache per show, default is 3.
* Provider server failure charts display what failed, when and why
* Provider server failure handler that pauses connecting the more a server fails
* Provider server activity graph and stats display how each are performing
* Built-in providers that have proved over time to deliver leading durability among its application class and third parties
* Built-in Kodi repository service to maintain SickGear add-ons, the first add-on is Watched State Updater
* Separate Plex server and Plex client settings, multiple LAN Plex server support, and Plex Home multiple user support
* Intelligent library updates target the actual Plex server that carries the show of an episode (multi LAN server)
* Communicate directly with NZBGet using a dedicated integration script
* Communicate directly with qBittorrent/Deluge/Transmission etc. using a dedicated integration script
* Proxy auto-config (PAC) support to define when a proxy is used instead of using one for every system request
* Change file date, to the date that the episode aired (yup, the feature was first created here for XBMC file sorting)
* Visual percentage progress of managed episodes
* Configure an episode status for removed media files
* Configurable default home page
Features above link to the UI; innovations also exist in core, we often inspire imitators, but you can [get the real deal!](https://github.com/SickGear/SickGear/wiki/Installation-Instructions)
Other features and worthy points;
* Stable, quality assured testing and development cycle
* Innovations that inspire imitators
* Compatible with any platform via a familiar web interface
* Most comprehensive selection of usenet and torrent sources
* Compatible with any platform via a familiar web interface
* Episode management
* Group shows into personalised sections
* View missed and upcoming shows at a glance with "day by day" and other layouts
* Group shows into personalised sections in a full show list view
* Automatic and manual search for availability of wanted episodes
* Set what episodes you want and how to receive them
* Uses well known established index sites to gather show information
* Searches for known alternatively named shows with a fallback to user edited names
* Searches for known alternatively numbered episodes with a fallback to user edited numbers
* Searches for known alternatively numbered seasons with a fallback to user edited numbers
* Forward search results to a downloader (e.g. NZBGet, SABNZBd, uTorrent, and others)
* Save search results to a "blackhole" folder that can be periodically scanned for taking action
* Post-process downloaded episodes into customisable layouts, with or without extra metadata
* Advanced Failed Download Handling (FDH)
* Overview of seasons, episodes, rating, version, airdate, episode status ([their meaning](https://github.com/SickGear/SickGear/wiki/Status-Modes))
* Processing nzb/torrents with your downloader application at your chosen qualities
* Subtitle management
* Automatic and manual search for availability of episodes you want
* Well known established sources are used to gather reliable show information
* Known and user added alternative show names can be searched
* Known and user added alternative numbered seasons and/or episodes can be searched
* Forward search results directly to clients like NZBGet, SABNZBd, qBitTorrent, Deluge and others
* Save search results to a "blackhole" folder, a place designated for clients to auto scan
* Built-in post processing of episodes, with renaming to custom folder and/or file names
* Fetch metadata like fanart, poster and banner images, nfo's for clients like Kodi, Plex and many more
* Native advanced automated handling of failed downloads to ensure a success after failure
* Overview of episodes, rating, version, airdate, episode status ([their meaning](https://github.com/SickGear/SickGear/wiki/Status-Modes))
* Support for specials and multi episode media files
* Automated subtitle management
* Notification
* Home Theater/NAS (Emby, Kodi, Plex, Syno, Tivo, and more)
* Home Theater/NAS (Emby, Kodi, Plex, Syno, Tivo, and more) can be notified to update their library
* Social notifiers (Trakt, Slack, Gitter, Discord, E-mail, and more)
* Device notifiers (Boxcar2, Notify My Android, Growl, Prowl, and more)
* Server friendly with minimal number of calls (e.g. one request per chosen snatch, not per result)
* Can recommend trendy and/or personally tailored shows from Trakt, IMDb, AniDB
* Server friendly with minimal number of API calls using both active and passive search tech
* Automated alternative show names and episode numbering from XEM
Some of our innovative features;
* Automated search after adding a show
* Desktop notifications
* Enhanced Anime features when adding shows
* Visual percentage progress of managed episodes
* Separate Plex server and Plex client settings
* Intelligent library updates that target Plex servers that list the show of an episode
* Configurable episode status for removed media files
* Configurable default home page
* Source providers
* User Interface
## Screenies
<table><thead></thead><tbody>
<tr align="center">
@ -103,15 +122,15 @@ If your pull request is a new feature, please try explaining its function to a l
## Contributors
#### To core
MidgetSpy, zoggy, 1337, Tolstyak, Mr_Orange, Bricky, JackDandy, Prinz23, Supremicus, adam111316, and zanaga
MidgetSpy, zoggy, 1337, Tolstyak, Mr_Orange, Bricky, JackDandy, Prinz23, Supremicus, adam111316, and zanaga
#### In other areas, testers and QA
Warm thanks to tehspede, CtrlAltDefeat, Mike, vergessen (betrayed), and Rawh for their keen eye, all around help, and making sure all manner of things work as expected
Warm thanks to tehspede, CtrlAltDefeat, Mike, vergessen (betrayed), and Rawh for their keen eye, all around help, and making sure all manner of things work as expected
#### Provider code
Thanks also, to unsung heroes that added source providers; Idan Gutman, Daniel Heimans, jkaberg, and Seedboy
Thanks also, to unsung heroes that added source providers; Idan Gutman, Daniel Heimans, jkaberg, and Seedboy
Finally, a massive thanks to all those that remain in the shadows, the quiet ones who welcome folk to special places, we salute you for your hospitality and for tirelessly keeping up operations.
Finally, a massive thanks to all those that remain in the shadows, the quiet ones who welcome folk to special places, we salute you for your hospitality and for tirelessly keeping up operations.
## Community
* <a href="https://gitter.im/SickGear/"><img src="https://badges.gitter.im/sickgear.svg"></a> web based (most likely one on one with a dev)

View file

@ -37,12 +37,14 @@ sys.path.insert(1, os.path.abspath('../lib'))
from sickbeard import helpers, encodingKludge as ek
from sickbeard import db, image_cache, logger, naming, metadata, providers, scene_exceptions, scene_numbering, \
scheduler, auto_post_processer, search_queue, search_propers, search_recent, search_backlog, \
show_queue, show_updater, subtitles, traktChecker, version_checker, indexermapper, classes, properFinder
from sickbeard.config import CheckSection, check_setting_int, check_setting_str, ConfigMigrator, minimax
show_queue, show_updater, subtitles, traktChecker, version_checker, indexermapper, classes, properFinder, \
watchedstate_queue
from sickbeard.config import check_section, check_setting_int, check_setting_str, ConfigMigrator, minimax
from sickbeard.common import SD, SKIPPED
from sickbeard.databases import mainDB, cache_db, failed_db
from sickbeard.exceptions import ex
from sickbeard.providers.generic import GenericProvider
from sickbeard.watchedstate import EmbyWatchedStateUpdater, PlexWatchedStateUpdater
from indexers.indexer_config import INDEXER_TVDB
from indexers.indexer_api import indexerApi
from indexers.indexer_exceptions import indexer_shownotfound, indexer_exception, indexer_error, \
@ -85,6 +87,9 @@ autoPostProcesserScheduler = None
subtitlesFinderScheduler = None
# traktCheckerScheduler = None
background_mapping_task = None
embyWatchedStateScheduler = None
plexWatchedStateScheduler = None
watchedStateQueueScheduler = None
provider_ping_thread_pool = {}
@ -226,12 +231,15 @@ DEFAULT_AUTOPOSTPROCESSER_FREQUENCY = 10
DEFAULT_RECENTSEARCH_FREQUENCY = 40
DEFAULT_BACKLOG_FREQUENCY = 21
DEFAULT_UPDATE_FREQUENCY = 1
DEFAULT_WATCHEDSTATE_FREQUENCY = 10
MIN_AUTOPOSTPROCESSER_FREQUENCY = 1
MIN_RECENTSEARCH_FREQUENCY = 10
MIN_BACKLOG_FREQUENCY = 7
MAX_BACKLOG_FREQUENCY = 42
MIN_UPDATE_FREQUENCY = 1
MIN_WATCHEDSTATE_FREQUENCY = 10
MAX_WATCHEDSTATE_FREQUENCY = 60
BACKLOG_DAYS = 7
SEARCH_UNAIRED = False
@ -257,6 +265,7 @@ NZBGET_CATEGORY = None
NZBGET_HOST = None
NZBGET_USE_HTTPS = False
NZBGET_PRIORITY = 100
NZBGET_SCRIPT_VERSION = None
SAB_USERNAME = None
SAB_PASSWORD = None
@ -276,8 +285,11 @@ TORRENT_VERIFY_CERT = False
USE_EMBY = False
EMBY_UPDATE_LIBRARY = False
EMBY_PARENT_MAPS = None
EMBY_HOST = None
EMBY_APIKEY = None
EMBY_WATCHEDSTATE_SCHEDULED = False
EMBY_WATCHEDSTATE_FREQUENCY = None
USE_KODI = False
KODI_ALWAYS_ON = True
@ -287,6 +299,7 @@ KODI_NOTIFY_ONSUBTITLEDOWNLOAD = False
KODI_UPDATE_LIBRARY = False
KODI_UPDATE_FULL = False
KODI_UPDATE_ONLYFIRST = False
KODI_PARENT_MAPS = None
KODI_HOST = ''
KODI_USERNAME = None
KODI_PASSWORD = None
@ -296,10 +309,13 @@ PLEX_NOTIFY_ONSNATCH = False
PLEX_NOTIFY_ONDOWNLOAD = False
PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = False
PLEX_UPDATE_LIBRARY = False
PLEX_PARENT_MAPS = None
PLEX_SERVER_HOST = None
PLEX_HOST = None
PLEX_USERNAME = None
PLEX_PASSWORD = None
PLEX_WATCHEDSTATE_SCHEDULED = False
PLEX_WATCHEDSTATE_FREQUENCY = None
USE_XBMC = False
XBMC_ALWAYS_ON = True
@ -565,8 +581,10 @@ def initialize(console_logging=True):
# global traktCheckerScheduler
global recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \
versionCheckScheduler, showQueueScheduler, searchQueueScheduler, \
properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, background_mapping_task, \
provider_ping_thread_pool
properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, \
background_mapping_task, provider_ping_thread_pool, \
embyWatchedStateScheduler, plexWatchedStateScheduler, watchedStateQueueScheduler, \
MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY, DEFAULT_WATCHEDSTATE_FREQUENCY
# Add Show Search
global RESULTS_SORTBY
# Add Show Defaults
@ -600,7 +618,8 @@ def initialize(console_logging=True):
ALLOW_HIGH_PRIORITY, SEARCH_UNAIRED, UNAIRED_RECENT_SEARCH_ONLY
# Search Settings/NZB search
global USE_NZBS, NZB_METHOD, NZB_DIR, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, \
NZBGET_USE_HTTPS, NZBGET_HOST, NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY
NZBGET_USE_HTTPS, NZBGET_HOST, NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, \
NZBGET_SCRIPT_VERSION
# Search Settings/Torrent search
global USE_TORRENTS, TORRENT_METHOD, TORRENT_DIR, TORRENT_HOST, TORRENT_USERNAME, TORRENT_PASSWORD, \
TORRENT_LABEL, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_VERIFY_CERT
@ -623,14 +642,16 @@ def initialize(console_logging=True):
global metadata_provider_dict, METADATA_KODI, METADATA_MEDE8ER, METADATA_XBMC, METADATA_MEDIABROWSER, \
METADATA_PS3, METADATA_TIVO, METADATA_WDTV, METADATA_XBMC_12PLUS
# Notification Settings/HT and NAS
global USE_EMBY, EMBY_UPDATE_LIBRARY, EMBY_HOST, EMBY_APIKEY, \
global USE_EMBY, EMBY_UPDATE_LIBRARY, EMBY_PARENT_MAPS, EMBY_HOST, EMBY_APIKEY, \
EMBY_WATCHEDSTATE_SCHEDULED, EMBY_WATCHEDSTATE_FREQUENCY, \
USE_KODI, KODI_ALWAYS_ON, KODI_UPDATE_LIBRARY, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, \
KODI_HOST, KODI_USERNAME, KODI_PASSWORD, KODI_NOTIFY_ONSNATCH, \
KODI_PARENT_MAPS, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, KODI_NOTIFY_ONSNATCH, \
KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, \
USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, \
XBMC_UPDATE_LIBRARY, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, \
USE_PLEX, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE_LIBRARY, PLEX_SERVER_HOST, \
USE_PLEX, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE_LIBRARY, PLEX_PARENT_MAPS, PLEX_SERVER_HOST, \
PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_HOST, \
PLEX_WATCHEDSTATE_SCHEDULED, PLEX_WATCHEDSTATE_FREQUENCY, \
USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, \
USE_NMJv2, NMJv2_HOST, NMJv2_DATABASE, NMJv2_DBLOC, \
USE_SYNOINDEX, \
@ -678,7 +699,7 @@ def initialize(console_logging=True):
'Growl', 'Prowl', 'Twitter', 'Slack', 'Discordapp', 'Boxcar2', 'NMJ', 'NMJv2',
'Synology', 'SynologyNotifier',
'pyTivo', 'NMA', 'Pushalot', 'Pushbullet', 'Subtitles'):
CheckSection(CFG, stanza)
check_section(CFG, stanza)
update_config = False
@ -908,6 +929,15 @@ def initialize(console_logging=True):
NZBGET_USE_HTTPS = bool(check_setting_int(CFG, 'NZBget', 'nzbget_use_https', 0))
NZBGET_PRIORITY = check_setting_int(CFG, 'NZBget', 'nzbget_priority', 100)
try:
ng_script_file = ek.ek(os.path.join, ek.ek(os.path.dirname, ek.ek(os.path.dirname, __file__)),
'autoProcessTV', 'SickGear-NG', 'SickGear-NG.py')
with open(ng_script_file, 'r') as ng:
text = ng.read()
NZBGET_SCRIPT_VERSION = re.search(r'__version__ =.*\'([0-9.]+)\'.*$', text, flags=re.M).group(1)
except (StandardError, Exception):
NZBGET_SCRIPT_VERSION = None
TORRENT_USERNAME = check_setting_str(CFG, 'TORRENT', 'torrent_username', '')
TORRENT_PASSWORD = check_setting_str(CFG, 'TORRENT', 'torrent_password', '')
TORRENT_HOST = check_setting_str(CFG, 'TORRENT', 'torrent_host', '')
@ -920,8 +950,13 @@ def initialize(console_logging=True):
USE_EMBY = bool(check_setting_int(CFG, 'Emby', 'use_emby', 0))
EMBY_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Emby', 'emby_update_library', 0))
EMBY_PARENT_MAPS = check_setting_str(CFG, 'Emby', 'emby_parent_maps', '')
EMBY_HOST = check_setting_str(CFG, 'Emby', 'emby_host', '')
EMBY_APIKEY = check_setting_str(CFG, 'Emby', 'emby_apikey', '')
EMBY_WATCHEDSTATE_SCHEDULED = bool(check_setting_int(CFG, 'Emby', 'emby_watchedstate_scheduled', 0))
EMBY_WATCHEDSTATE_FREQUENCY = minimax(check_setting_int(
CFG, 'Emby', 'emby_watchedstate_frequency', DEFAULT_WATCHEDSTATE_FREQUENCY),
DEFAULT_WATCHEDSTATE_FREQUENCY, MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY)
USE_KODI = bool(check_setting_int(CFG, 'Kodi', 'use_kodi', 0))
KODI_ALWAYS_ON = bool(check_setting_int(CFG, 'Kodi', 'kodi_always_on', 1))
@ -931,6 +966,7 @@ def initialize(console_logging=True):
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_PARENT_MAPS = check_setting_str(CFG, 'Kodi', 'kodi_parent_maps', '')
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', '')
@ -952,10 +988,15 @@ def initialize(console_logging=True):
PLEX_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_ondownload', 0))
PLEX_NOTIFY_ONSUBTITLEDOWNLOAD = bool(check_setting_int(CFG, 'Plex', 'plex_notify_onsubtitledownload', 0))
PLEX_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Plex', 'plex_update_library', 0))
PLEX_PARENT_MAPS = check_setting_str(CFG, 'Plex', 'plex_parent_maps', '')
PLEX_SERVER_HOST = check_setting_str(CFG, 'Plex', 'plex_server_host', '')
PLEX_HOST = check_setting_str(CFG, 'Plex', 'plex_host', '')
PLEX_USERNAME = check_setting_str(CFG, 'Plex', 'plex_username', '')
PLEX_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_password', '')
PLEX_WATCHEDSTATE_SCHEDULED = bool(check_setting_int(CFG, 'Plex', 'plex_watchedstate_scheduled', 0))
PLEX_WATCHEDSTATE_FREQUENCY = minimax(check_setting_int(
CFG, 'Plex', 'plex_watchedstate_frequency', DEFAULT_WATCHEDSTATE_FREQUENCY),
DEFAULT_WATCHEDSTATE_FREQUENCY, MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY)
USE_GROWL = bool(check_setting_int(CFG, 'Growl', 'use_growl', 0))
GROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsnatch', 0))
@ -1038,7 +1079,6 @@ def initialize(console_logging=True):
TRAKT_ACCOUNTS = TraktAPI.read_config_string(check_setting_str(CFG, 'Trakt', 'trakt_accounts', ''))
TRAKT_MRU = check_setting_str(CFG, 'Trakt', 'trakt_mru', '')
CheckSection(CFG, 'pyTivo')
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '')
PYTIVO_SHARE_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_share_name', '')
@ -1341,7 +1381,8 @@ def initialize(console_logging=True):
cycleTime=datetime.timedelta(seconds=3),
threadName='SEARCHQUEUE')
update_interval = datetime.timedelta(minutes=(RECENTSEARCH_FREQUENCY, 1)[4489 == RECENTSEARCH_FREQUENCY])
# enter 4490 (was 4489) for experimental internal provider frequencies
update_interval = datetime.timedelta(minutes=(RECENTSEARCH_FREQUENCY, 1)[4499 == RECENTSEARCH_FREQUENCY])
recentSearchScheduler = scheduler.Scheduler(
search_recent.RecentSearcher(),
cycleTime=update_interval,
@ -1407,6 +1448,23 @@ def initialize(console_logging=True):
background_mapping_task = threading.Thread(name='LOAD-MAPPINGS', target=indexermapper.load_mapped_ids)
watchedStateQueueScheduler = scheduler.Scheduler(
watchedstate_queue.WatchedStateQueue(),
cycleTime=datetime.timedelta(seconds=3),
threadName='WATCHEDSTATEQUEUE')
embyWatchedStateScheduler = scheduler.Scheduler(
EmbyWatchedStateUpdater(),
cycleTime=datetime.timedelta(minutes=EMBY_WATCHEDSTATE_FREQUENCY),
run_delay=datetime.timedelta(minutes=5),
threadName='EMBYWATCHEDSTATE')
plexWatchedStateScheduler = scheduler.Scheduler(
PlexWatchedStateUpdater(),
cycleTime=datetime.timedelta(minutes=PLEX_WATCHEDSTATE_FREQUENCY),
run_delay=datetime.timedelta(minutes=5),
threadName='PLEXWATCHEDSTATE')
__INITIALIZED__ = True
return True
@ -1416,7 +1474,8 @@ def enabled_schedulers(is_init=False):
for s in ([], [events])[is_init] + \
[recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler,
versionCheckScheduler, showQueueScheduler, searchQueueScheduler, properFinderScheduler,
autoPostProcesserScheduler, subtitlesFinderScheduler] + \
autoPostProcesserScheduler, subtitlesFinderScheduler,
embyWatchedStateScheduler, plexWatchedStateScheduler, watchedStateQueueScheduler] + \
([events], [])[is_init]:
yield s
@ -1724,8 +1783,11 @@ def save_config():
new_config['Emby'] = {}
new_config['Emby']['use_emby'] = int(USE_EMBY)
new_config['Emby']['emby_update_library'] = int(EMBY_UPDATE_LIBRARY)
new_config['Emby']['emby_parent_maps'] = EMBY_PARENT_MAPS
new_config['Emby']['emby_host'] = EMBY_HOST
new_config['Emby']['emby_apikey'] = EMBY_APIKEY
new_config['Emby']['emby_watchedstate_scheduled'] = int(EMBY_WATCHEDSTATE_SCHEDULED)
new_config['Emby']['emby_watchedstate_frequency'] = int(EMBY_WATCHEDSTATE_FREQUENCY)
new_config['Kodi'] = {}
new_config['Kodi']['use_kodi'] = int(USE_KODI)
@ -1733,6 +1795,7 @@ def save_config():
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_parent_maps'] = KODI_PARENT_MAPS
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)
@ -1745,11 +1808,14 @@ def save_config():
new_config['Plex']['plex_username'] = PLEX_USERNAME
new_config['Plex']['plex_password'] = helpers.encrypt(PLEX_PASSWORD, ENCRYPTION_VERSION)
new_config['Plex']['plex_update_library'] = int(PLEX_UPDATE_LIBRARY)
new_config['Plex']['plex_parent_maps'] = PLEX_PARENT_MAPS
new_config['Plex']['plex_server_host'] = PLEX_SERVER_HOST
new_config['Plex']['plex_notify_onsnatch'] = int(PLEX_NOTIFY_ONSNATCH)
new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD)
new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD)
new_config['Plex']['plex_host'] = PLEX_HOST
new_config['Plex']['plex_watchedstate_scheduled'] = int(PLEX_WATCHEDSTATE_SCHEDULED)
new_config['Plex']['plex_watchedstate_frequency'] = int(PLEX_WATCHEDSTATE_FREQUENCY)
new_config['XBMC'] = {}
new_config['XBMC']['use_xbmc'] = int(USE_XBMC)

View file

@ -23,21 +23,22 @@ from sickbeard import logger, processTV
from sickbeard import encodingKludge as ek
class PostProcesser():
class PostProcesser:
def __init__(self):
self.amActive = False
@staticmethod
def check_paused():
if sickbeard.PROCESS_AUTOMATICALLY:
return False
return True
def is_enabled():
return sickbeard.PROCESS_AUTOMATICALLY
def run(self):
if not sickbeard.PROCESS_AUTOMATICALLY:
return
if self.is_enabled():
self.amActive = True
self._main()
self.amActive = False
self.amActive = True
@staticmethod
def _main():
if not ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR):
logger.log(u"Automatic post-processing attempted but dir %s doesn't exist" % sickbeard.TV_DOWNLOAD_DIR,
@ -50,5 +51,3 @@ class PostProcesser():
return
processTV.processDir(sickbeard.TV_DOWNLOAD_DIR, is_basedir=True)
self.amActive = False

View file

@ -17,6 +17,7 @@
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import re
import datetime
import os
import sickbeard
from sickbeard.common import Quality
@ -379,3 +380,48 @@ class ImageUrlList(list):
if self._is_cache_item(x) and url == x[0]:
super(ImageUrlList, self).remove(x)
break
if 'nt' == os.name:
import ctypes
class WinEnv:
def __init__(self):
pass
@staticmethod
def get_environment_variable(name):
name = unicode(name) # ensures string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
result = None
if n:
buf = ctypes.create_unicode_buffer(u'\0'*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
result = buf.value
return result
def __getitem__(self, key):
return self.get_environment_variable(key)
def get(self, key, default=None):
r = self.get_environment_variable(key)
return r if r is not None else default
sickbeard.ENV = WinEnv()
else:
class LinuxEnv(object):
def __init__(self, environ):
self.environ = environ
def __getitem__(self, key):
v = self.environ.get(key)
try:
return v.decode(SYS_ENCODING) if isinstance(v, str) else v
except (UnicodeDecodeError, UnicodeEncodeError):
return v
def get(self, key, default=None):
v = self[key]
return v if v is not None else default
sickbeard.ENV = LinuxEnv(os.environ)

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,22 @@
# /tests/_devenv.py
#
# To trigger dev env
#
# import _devenv as devenv
#
__remotedebug__ = True
if __remotedebug__:
import sys
sys.path.append('C:\Program Files\JetBrains\PyCharm 2017.2.1\debug-eggs\pycharm-debug.egg')
import pydevd
def setup_devenv(state):
pydevd.settrace('localhost', port=(65001, 65000)[bool(state)], stdoutToServer=True, stderrToServer=True,
suspend=False)
def stop():
pydevd.stoptrace()

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="service.sickgear.watchedstate.updater" name="SickGear Watched State Updater" version="1.0.3" provider-name="SickGear">
<requires>
<import addon="xbmc.python" version="2.19.0" />
<import addon="xbmc.json" version="6.20.0" />
<import addon="xbmc.addon" version="14.0.0" />
</requires>
<extension point="xbmc.service" library="service.py" start="login" />
<extension point="xbmc.python.pluginsource" library="service.py" >
<provides>executable</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en">SickGear Watched State Updater</summary>
<description lang="en">This Add-on notifies SickGear when an episode watched state is changed in Kodi</description>
<platform>all</platform>
<language>en</language>
<disclaimer/>
<license/>
<forum/>
<website>https://github.com/sickgear/sickgear</website>
<email/>
<nofanart>true</nofanart>
<source>https://github.com/sickgear/sickgear</source>
<assets>
<icon>icon.png</icon>
</assets>
<news>[B]1.0.0[/B] (2017-10-04)
- Initial release
[B]1.0.2[/B] (2017-11-15)
- Devel release for an SG API change
[B]1.0.3[/B] (2018-02-28)
- Add episodeid to payload
</news>
</extension>
</addon>

View file

@ -0,0 +1,2 @@
[B]1.0.0[/B] (2017-10-04)
- Initial release

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<strings>
<string id="32000">General</string>
<string id="32011">Action Notifications</string>
<string id="32012">Error Notifications</string>
<string id="32021">Verbose Logs</string>
<string id="32100">Servers</string>
<string id="32111">SickGear IP</string>
<string id="32112">SickGear Port</string>
<string id="32121">Kodi IP</string>
<string id="32122">Kodi JSON RPC Port</string>
<string id="32500">The following required Kodi settings should already be enabled:</string>
<string id="32511">At "System / Service(s) settings / Control (aka Remote control)"</string>
<string id="32512">* Allow remote control from/by applications/programs on this system</string>
<string id="32513">* Allow remote control from/by applications/programs on other systems</string>
</strings>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<settings>
<category label="32000">
<setting label="32011" type="bool" id="action_notification" default="true" />
<setting label="32012" type="bool" id="error_notification" default="true" />
<setting label="32021" type="bool" id="verbose_log" default="true" />
<setting label="32500" type="lsep" />
<setting label="32511" type="lsep" />
<setting label="32512" type="lsep" />
<setting label="32513" type="lsep" />
</category>
<category label="32100">
<setting label="32111" type="ipaddress" id="sickgear_ip" default="127.0.0.1" />
<setting label="32112" type="number" id="sickgear_port" default="8081" />
<setting label="32121" type="ipaddress" id="kodi_ip" default="127.0.0.1" />
<setting label="32122" type="number" id="kodi_port" default="9090" />
</category>
</settings>

View file

@ -0,0 +1,361 @@
# coding=utf-8
#
# 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/>.
try:
import json as json
except (StandardError, Exception):
import simplejson as json
from os import path, sep
import datetime
import socket
import time
import traceback
import urllib
import urllib2
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs
class SickGearWatchedStateUpdater:
def __init__(self):
self.wait_onstartup = 4000
icon_size = '%s'
try:
if 1350 > xbmcgui.Window.getWidth(xbmcgui.Window()):
icon_size += '-sm'
except (StandardError, Exception):
pass
icon = 'special://home/addons/service.sickgear.watchedstate.updater/resources/icon-%s.png' % icon_size
self.addon = xbmcaddon.Addon()
self.red_logo = icon % 'red'
self.green_logo = icon % 'green'
self.black_logo = icon % 'black'
self.addon_name = self.addon.getAddonInfo('name')
self.kodi_ip = self.addon.getSetting('kodi_ip')
self.kodi_port = int(self.addon.getSetting('kodi_port'))
self.kodi_events = None
self.sock_kodi = None
def run(self):
"""
Main start
:return:
:rtype:
"""
if not self.enable_kodi_allow_remote():
return
self.sock_kodi = socket.socket()
self.sock_kodi.setblocking(True)
xbmc.sleep(self.wait_onstartup)
try:
self.sock_kodi.connect((self.kodi_ip, self.kodi_port))
except (StandardError, Exception) as e:
return self.report_contact_fail(e)
self.log('Started')
self.notify('Started in background')
self.kodi_events = xbmc.Monitor()
sock_buffer, depth, methods, method = '', 0, {'VideoLibrary.OnUpdate': self.video_library_on_update}, None
# socks listener parsing Kodi json output into action to perform
while not self.kodi_events.abortRequested():
chunk = self.sock_kodi.recv(1)
sock_buffer += chunk
if chunk in '{}':
if '{' == chunk:
depth += 1
else:
depth -= 1
if not depth:
json_msg = json.loads(sock_buffer)
try:
method = json_msg.get('method')
method_handler = methods[method]
method_handler(json_msg)
except KeyError:
if 'System.OnQuit' == method:
break
if __dev__:
self.log('pass on event: %s' % json_msg.get('method'))
sock_buffer = ''
self.sock_kodi.close()
del self.kodi_events
self.log('Stopped')
def is_enabled(self, name):
"""
Return state of an Add-on setting as Boolean
:param name: Name of Addon setting
:type name: String
:return: Success as True if addon setting is enabled, else False
:rtype: Bool
"""
return 'true' == self.addon.getSetting(name)
def log(self, msg, error=False):
"""
Add a message to the Kodi logging system (provided setting allows it)
:param msg: Text to add to log file
:type msg: String
:param error: Specify whether text indicates an error or action
:type error: Boolean
:return:
:rtype:
"""
if self.is_enabled('verbose_log'):
xbmc.log('[%s]:: %s' % (self.addon_name, msg), (xbmc.LOGNOTICE, xbmc.LOGERROR)[error])
def notify(self, msg, period=4, error=None):
"""
Invoke the Kodi onscreen notification panel with a message (provided setting allows it)
:param msg: Text to display in panel
:type msg: String
:param period: Wait seconds before closing dialog
:type period: Integer
:param error: Specify whether text indicates an error or action
:type error: Boolean
:return:
:rtype:
"""
if not error and self.is_enabled('action_notification') or (error and self.is_enabled('error_notification')):
xbmc.executebuiltin('Notification(%s, "%s", %s, %s)' % (
self.addon_name, msg, 1000 * period,
((self.green_logo, self.red_logo)[any([error])], self.black_logo)[None is error]))
@staticmethod
def ex(e):
return '\n'.join(['\nEXCEPTION Raised: --> Python callback/script returned the following error <--',
'Error type: <type \'{0}\'>',
'Error content: {1!r}',
'{2}',
'--> End of Python script error report <--\n'
]).format(type(e).__name__, e.args, traceback.format_exc())
def report_contact_fail(self, e):
msg = 'Failed to contact Kodi at %s:%s' % (self.kodi_ip, self.kodi_port)
self.log('%s %s' % (msg, self.ex(e)), error=True)
self.notify(msg, period=20, error=True)
def kodi_request(self, params):
params.update(dict(jsonrpc='2.0', id='SickGear'))
try:
response = xbmc.executeJSONRPC(json.dumps(params))
except (StandardError, Exception) as e:
return self.report_contact_fail(e)
try:
return json.loads(response)
except UnicodeDecodeError:
return json.loads(response.decode('utf-8', 'ignore'))
def video_library_on_update(self, json_msg):
"""
Actions to perform for: Kodi Notifications / VideoLibrary/ VideoLibrary.OnUpdate
invoked in Kodi when: A video item has been updated
source: http://kodi.wiki/view/JSON-RPC_API/v8#VideoLibrary.OnUpdate
:param json_msg: A JSON parsed from socks
:type json_msg: String
:return:
:rtype:
"""
try:
# note: this is called multiple times when a season is marked as un-/watched
if 'episode' == json_msg['params']['data']['item']['type']:
media_id = json_msg['params']['data']['item']['id']
play_count = json_msg['params']['data']['playcount']
json_resp = self.kodi_request(dict(
method='Profiles.GetCurrentProfile'))
current_profile = json_resp['result']['label']
json_resp = self.kodi_request(dict(
method='VideoLibrary.GetEpisodeDetails',
params=dict(episodeid=media_id, properties=['file'])))
path_file = json_resp['result']['episodedetails']['file'].encode('utf-8')
self.update_sickgear(media_id, path_file, play_count, current_profile)
except (StandardError, Exception):
pass
def update_sickgear(self, media_id, path_file, play_count, profile):
self.notify('Update sent to SickGear')
url = 'http://%s:%s/update_watched_state_kodi/' % (
self.addon.getSetting('sickgear_ip'), self.addon.getSetting('sickgear_port'))
self.log('Notify state to %s with path_file=%s' % (url, path_file))
msg_bad = 'Failed to contact SickGear on port %s at %s' % (
self.addon.getSetting('sickgear_port'), self.addon.getSetting('sickgear_ip'))
payload_json = self.payload_prep(dict(media_id=media_id, path_file=path_file, played=play_count, label=profile))
if payload_json:
payload = urllib.urlencode(dict(payload=payload_json))
try:
rq = urllib2.Request(url, data=payload)
r = urllib2.urlopen(rq)
response = json.load(r)
r.close()
if 'OK' == r.msg:
self.payload_prep(response)
if not all(response.values()):
msg = 'Success, watched state updated'
else:
msg = 'Success, %s/%s watched stated updated' % (
len([v for v in response.values() if v]), len(response.values()))
self.log(msg)
self.notify(msg, error=False)
else:
msg_bad = 'Failed to update watched state'
self.log(msg_bad)
self.notify(msg_bad, error=True)
except (urllib2.URLError, IOError) as e:
self.log(u'Couldn\'t contact SickGear %s' % self.ex(e), error=True)
self.notify(msg_bad, error=True, period=15)
except (StandardError, Exception) as e:
self.log(u'Couldn\'t contact SickGear %s' % self.ex(e), error=True)
self.notify(msg_bad, error=True, period=15)
@staticmethod
def payload_prep(payload):
name = 'sickgear_buffer.txt'
# try to locate /temp at parent location
path_temp = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'temp')
path_data = path.join(path_temp, name)
data_pool = {}
if xbmcvfs.exists(path_data):
fh = None
try:
fh = xbmcvfs.File(path_data)
data_pool = json.load(fh)
except (StandardError, Exception):
pass
fh and fh.close()
temp_ok = True
if not any([data_pool]):
temp_ok = xbmcvfs.exists(path_temp) or xbmcvfs.exists(path.join(path_temp, sep))
if not temp_ok:
temp_ok = xbmcvfs.mkdirs(path_temp)
response_data = False
for k, v in payload.items():
if response_data or k in data_pool:
response_data = True
if not v:
# whether no fail response or bad input, remove this from data
data_pool.pop(k)
elif isinstance(v, basestring):
# error so retry next time
continue
if not response_data:
ts_now = time.mktime(datetime.datetime.now().timetuple())
timeout = 100
while ts_now in data_pool and timeout:
ts_now = time.mktime(datetime.datetime.now().timetuple())
timeout -= 1
max_payload = 50-1
for k in list(data_pool.keys())[max_payload:]:
data_pool.pop(k)
payload.update(dict(date_watched=ts_now))
data_pool.update({ts_now: payload})
output = json.dumps(data_pool)
if temp_ok:
fh = None
try:
fh = xbmcvfs.File(path_data, 'w')
fh.write(output)
except (StandardError, Exception):
pass
fh and fh.close()
return output
def enable_kodi_allow_remote(self):
try:
# setting esenabled: allow remote control by programs on this system
# setting esallinterfaces: allow remote control by programs on other systems
settings = [dict(esenabled=True), dict(esallinterfaces=True)]
for setting in settings:
if not self.kodi_request(dict(
method='Settings.SetSettingValue',
params=dict(setting='services.%s' % setting.keys()[0], value=setting.values()[0])
)).get('result', {}):
settings[setting] = self.kodi_request(dict(
method='Settings.GetSettingValue',
params=dict(setting='services.%s' % setting.keys()[0])
)).get('result', {}).get('value')
except (StandardError, Exception):
return
setting_states = [setting.values()[0] for setting in settings]
if not all(setting_states):
if not (any(setting_states)):
msg = 'Please enable *all* Kodi settings to allow remote control by programs...'
else:
msg = 'Please enable Kodi setting to allow remote control by programs on other systems'
msg = 'Failed startup. %s in system service/remote control' % msg
self.log(msg, error=True)
self.notify(msg, period=20, error=True)
return
return True
__dev__ = True
if __dev__:
try:
# noinspection PyProtectedMember
import _devenv as devenv
except ImportError:
__dev__ = False
if 1 < len(sys.argv):
if __dev__:
devenv.setup_devenv(False)
if sys.argv[2].endswith('send_all'):
print('>>>>>> TESTTESTTEST')
elif __name__ == '__main__':
if __dev__:
devenv.setup_devenv(True)
WSU = SickGearWatchedStateUpdater()
WSU.run()
del WSU
if __dev__:
devenv.stop()

View file

@ -41,19 +41,6 @@ cpu_presets = {'DISABLED': 0, 'LOW': 0.01, 'NORMAL': 0.05, 'HIGH': 0.1}
MULTI_EP_RESULT = -1
SEASON_RESULT = -2
# Notification Types
NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2
NOTIFY_SUBTITLE_DOWNLOAD = 3
NOTIFY_GIT_UPDATE = 4
NOTIFY_GIT_UPDATE_TEXT = 5
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
UNKNOWN = -1 # should never happen
UNAIRED = 1 # episodes that haven't aired yet

View file

@ -49,7 +49,7 @@ naming_sep_type = (' - ', ' ')
naming_sep_type_text = (' - ', 'space')
def change_HTTPS_CERT(https_cert):
def change_https_cert(https_cert):
if https_cert == '':
sickbeard.HTTPS_CERT = ''
return True
@ -64,7 +64,7 @@ def change_HTTPS_CERT(https_cert):
return True
def change_HTTPS_KEY(https_key):
def change_https_key(https_key):
if https_key == '':
sickbeard.HTTPS_KEY = ''
return True
@ -79,7 +79,7 @@ def change_HTTPS_KEY(https_key):
return True
def change_LOG_DIR(log_dir, web_log):
def change_log_dir(log_dir, web_log):
log_dir_changed = False
abs_log_dir = os.path.normpath(os.path.join(sickbeard.DATA_DIR, log_dir))
web_log_value = checkbox_to_value(web_log)
@ -102,7 +102,7 @@ def change_LOG_DIR(log_dir, web_log):
return True
def change_NZB_DIR(nzb_dir):
def change_nzb_dir(nzb_dir):
if nzb_dir == '':
sickbeard.NZB_DIR = ''
return True
@ -117,7 +117,7 @@ def change_NZB_DIR(nzb_dir):
return True
def change_TORRENT_DIR(torrent_dir):
def change_torrent_dir(torrent_dir):
if torrent_dir == '':
sickbeard.TORRENT_DIR = ''
return True
@ -132,7 +132,7 @@ def change_TORRENT_DIR(torrent_dir):
return True
def change_TV_DOWNLOAD_DIR(tv_download_dir):
def change_tv_download_dir(tv_download_dir):
if tv_download_dir == '':
sickbeard.TV_DOWNLOAD_DIR = ''
return True
@ -147,16 +147,17 @@ def change_TV_DOWNLOAD_DIR(tv_download_dir):
return True
def change_AUTOPOSTPROCESSER_FREQUENCY(freq):
def schedule_autopostprocesser(freq):
sickbeard.AUTOPOSTPROCESSER_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_AUTOPOSTPROCESSER_FREQUENCY)
if sickbeard.AUTOPOSTPROCESSER_FREQUENCY < sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY:
sickbeard.AUTOPOSTPROCESSER_FREQUENCY = sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY
sickbeard.autoPostProcesserScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.AUTOPOSTPROCESSER_FREQUENCY)
sickbeard.autoPostProcesserScheduler.set_paused_state()
def change_RECENTSEARCH_FREQUENCY(freq):
def schedule_recentsearch(freq):
sickbeard.RECENTSEARCH_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_RECENTSEARCH_FREQUENCY)
if sickbeard.RECENTSEARCH_FREQUENCY < sickbeard.MIN_RECENTSEARCH_FREQUENCY:
@ -165,13 +166,14 @@ def change_RECENTSEARCH_FREQUENCY(freq):
sickbeard.recentSearchScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.RECENTSEARCH_FREQUENCY)
def change_BACKLOG_FREQUENCY(freq):
sickbeard.BACKLOG_FREQUENCY = minimax(freq, sickbeard.DEFAULT_BACKLOG_FREQUENCY, sickbeard.MIN_BACKLOG_FREQUENCY, sickbeard.MAX_BACKLOG_FREQUENCY)
def schedule_backlog(freq):
sickbeard.BACKLOG_FREQUENCY = minimax(freq, sickbeard.DEFAULT_BACKLOG_FREQUENCY,
sickbeard.MIN_BACKLOG_FREQUENCY, sickbeard.MAX_BACKLOG_FREQUENCY)
sickbeard.backlogSearchScheduler.action.cycleTime = sickbeard.BACKLOG_FREQUENCY
def change_UPDATE_FREQUENCY(freq):
def schedule_update(freq):
sickbeard.UPDATE_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_UPDATE_FREQUENCY)
if sickbeard.UPDATE_FREQUENCY < sickbeard.MIN_UPDATE_FREQUENCY:
@ -180,27 +182,25 @@ def change_UPDATE_FREQUENCY(freq):
sickbeard.versionCheckScheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_FREQUENCY)
def change_VERSION_NOTIFY(version_notify):
oldSetting = sickbeard.VERSION_NOTIFY
def schedule_version_notify(version_notify):
old_setting = sickbeard.VERSION_NOTIFY
sickbeard.VERSION_NOTIFY = version_notify
if not version_notify:
sickbeard.NEWEST_VERSION_STRING = None
if not oldSetting and version_notify:
if not old_setting and version_notify:
sickbeard.versionCheckScheduler.action.run()
def change_DOWNLOAD_PROPERS(download_propers):
if sickbeard.DOWNLOAD_PROPERS == download_propers:
return
sickbeard.DOWNLOAD_PROPERS = download_propers
sickbeard.properFinderScheduler.check_paused()
def schedule_download_propers(download_propers):
if sickbeard.DOWNLOAD_PROPERS != download_propers:
sickbeard.DOWNLOAD_PROPERS = download_propers
sickbeard.properFinderScheduler.set_paused_state()
def change_USE_TRAKT(use_trakt):
def schedule_trakt(use_trakt):
if sickbeard.USE_TRAKT == use_trakt:
return
@ -216,22 +216,40 @@ def change_USE_TRAKT(use_trakt):
# pass
def change_USE_SUBTITLES(use_subtitles):
if sickbeard.USE_SUBTITLES == use_subtitles:
return
sickbeard.USE_SUBTITLES = use_subtitles
sickbeard.subtitlesFinderScheduler.check_paused()
def schedule_subtitles(use_subtitles):
if sickbeard.USE_SUBTITLES != use_subtitles:
sickbeard.USE_SUBTITLES = use_subtitles
sickbeard.subtitlesFinderScheduler.set_paused_state()
def CheckSection(CFG, sec):
def schedule_emby_watched(emby_watched_interval):
emby_watched_freq = minimax(emby_watched_interval, sickbeard.DEFAULT_WATCHEDSTATE_FREQUENCY,
0, sickbeard.MAX_WATCHEDSTATE_FREQUENCY)
if emby_watched_freq and emby_watched_freq != sickbeard.EMBY_WATCHEDSTATE_FREQUENCY:
sickbeard.EMBY_WATCHEDSTATE_FREQUENCY = emby_watched_freq
sickbeard.embyWatchedStateScheduler.cycleTime = datetime.timedelta(minutes=emby_watched_freq)
sickbeard.EMBY_WATCHEDSTATE_SCHEDULED = bool(emby_watched_freq)
sickbeard.embyWatchedStateScheduler.set_paused_state()
def schedule_plex_watched(plex_watched_interval):
plex_watched_freq = minimax(plex_watched_interval, sickbeard.DEFAULT_WATCHEDSTATE_FREQUENCY,
0, sickbeard.MAX_WATCHEDSTATE_FREQUENCY)
if plex_watched_freq and plex_watched_freq != sickbeard.PLEX_WATCHEDSTATE_FREQUENCY:
sickbeard.PLEX_WATCHEDSTATE_FREQUENCY = plex_watched_freq
sickbeard.plexWatchedStateScheduler.cycleTime = datetime.timedelta(minutes=plex_watched_freq)
sickbeard.PLEX_WATCHEDSTATE_SCHEDULED = bool(plex_watched_freq)
sickbeard.plexWatchedStateScheduler.set_paused_state()
def check_section(cfg, section):
""" Check if INI section exists, if not create it """
try:
CFG[sec]
return True
except:
CFG[sec] = {}
if section not in cfg:
cfg[section] = {}
return False
return True
def checkbox_to_value(option, value_on=1, value_off=0):
@ -284,7 +302,7 @@ 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 [host.strip() for host in hosts.split(',')]:
if cur_host:
cleaned_host = clean_host(cur_host, default_port)
if cleaned_host:
@ -324,12 +342,25 @@ def clean_url(url, add_slash=True):
return cleaned_url
def kv_csv(data, default=''):
"""
Returns a cleansed CSV string of key value pairs
Elements must have one '=' in order to be returned
Elements are stripped of leading/trailing whitespace but may contain whitespace (e.g. "tv shows")
"""
if not isinstance(data, basestring):
return default
return ','.join(['='.join(i.strip() for i in i.split('=')) for i in data.split(',')
if 1 == len(re.findall('=', i)) and all(i.replace(' ', '').split('='))])
def to_int(val, default=0):
""" Return int value of val or default on error """
try:
val = int(val)
except:
except(StandardError, Exception):
val = default
return val
@ -351,11 +382,11 @@ def minimax(val, default, low, high):
def check_setting_int(config, cfg_name, item_name, def_val):
try:
my_val = int(config[cfg_name][item_name])
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = my_val
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
logger.log('%s -> %s' % (item_name, my_val), logger.DEBUG)
@ -365,11 +396,11 @@ def check_setting_int(config, cfg_name, item_name, def_val):
def check_setting_float(config, cfg_name, item_name, def_val):
try:
my_val = float(config[cfg_name][item_name])
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = my_val
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
@ -391,11 +422,11 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True):
try:
my_val = helpers.decrypt(config[cfg_name][item_name], encryption_version)
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = helpers.encrypt(my_val, encryption_version)
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = helpers.encrypt(my_val, encryption_version)
@ -407,7 +438,7 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True):
return (my_val, def_val)['None' == my_val]
class ConfigMigrator():
class ConfigMigrator:
def __init__(self, config_obj):
"""
Initializes a config migrator that can take the config from the version indicated in the config
@ -491,8 +522,8 @@ class ConfigMigrator():
sickbeard.NAMING_MULTI_EP = int(check_setting_int(self.config_obj, 'General', 'naming_multi_ep_type', 1))
# 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')
my_db = db.DBConnection()
season_folder_shows = my_db.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:
@ -518,7 +549,7 @@ class ConfigMigrator():
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')
my_db.action('UPDATE tv_shows SET flatten_folders = 0')
sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders()
@ -534,11 +565,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',
naming_ep_tmpl = ('%Sx%0E',
's%0Se%0E',
'S%0SE%0E',
'%0Sx%0E')
naming_sep_type = (' - ', ' ')
naming_sep_tmpl = (' - ', ' ')
# set up our data to use
if use_periods:
@ -555,29 +586,29 @@ class ConfigMigrator():
if abd:
ep_string = abd_string
else:
ep_string = naming_ep_type[ep_type]
ep_string = naming_ep_tmpl[ep_type]
finalName = ''
final_name = ''
# start with the show name
if use_show_name:
finalName += show_name + naming_sep_type[sep_type]
final_name += show_name + naming_sep_tmpl[sep_type]
# add the season/ep stuff
finalName += ep_string
final_name += ep_string
# add the episode name
if use_ep_name:
finalName += naming_sep_type[sep_type] + ep_name
final_name += naming_sep_tmpl[sep_type] + ep_name
# add the quality
if use_quality:
finalName += naming_sep_type[sep_type] + ep_quality
final_name += naming_sep_tmpl[sep_type] + ep_quality
if use_periods:
finalName = re.sub('\s+', '.', finalName)
final_name = re.sub('\s+', '.', final_name)
return finalName
return final_name
# Migration v2: Dummy migration to sync backup number with config version number
def _migrate_v2(self):
@ -589,7 +620,8 @@ class ConfigMigrator():
Reads in the old naming settings from your config and generates a new config template from them.
"""
# get the old settings from the file and store them in the new variable names
for prov in [curProvider for curProvider in sickbeard.providers.sortedProviderList() if curProvider.name == 'omgwtfnzbs']:
for prov in [curProvider for curProvider in sickbeard.providers.sortedProviderList()
if 'omgwtfnzbs' == curProvider.name]:
prov.username = check_setting_str(self.config_obj, 'omgwtfnzbs', 'omgwtfnzbs_uid', '')
prov.api_key = check_setting_str(self.config_obj, 'omgwtfnzbs', 'omgwtfnzbs_key', '')
@ -662,7 +694,7 @@ class ConfigMigrator():
use_banner = bool(check_setting_int(self.config_obj, 'General', 'use_banner', 0))
def _migrate_metadata(metadata, metadata_name, use_banner):
def _migrate_metadata(metadata, metadata_name, banner):
cur_metadata = metadata.split('|')
# if target has the old number of values, do upgrade
if len(cur_metadata) == 6:
@ -673,9 +705,9 @@ class ConfigMigrator():
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,
# if user was using banner to override the poster,
# instead enable the banner option and deactivate poster
if metadata_name == 'XBMC' and use_banner:
if 'XBMC' == metadata_name and banner:
cur_metadata[4], cur_metadata[3] = cur_metadata[3], '0'
# write new format
metadata = '|'.join(cur_metadata)
@ -723,7 +755,8 @@ class ConfigMigrator():
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):
@staticmethod
def _migrate_v8():
# removing settings from gui and making it a hidden debug option
sickbeard.RECENTSEARCH_STARTUP = False
@ -731,7 +764,8 @@ class ConfigMigrator():
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', '')
def _migrate_v10(self):
@staticmethod
def _migrate_v10():
# reset backlog frequency to default
sickbeard.BACKLOG_FREQUENCY = sickbeard.DEFAULT_BACKLOG_FREQUENCY
@ -741,9 +775,11 @@ class ConfigMigrator():
else:
sickbeard.SHOWLIST_TAGVIEW = 'default'
def _migrate_v12(self):
@staticmethod
def _migrate_v12():
# add words to ignore list and insert spaces to improve the ui config readability
words_to_add = ['hevc', 'reenc', 'x265', 'danish', 'deutsch', 'flemish', 'italian', 'nordic', 'norwegian', 'portuguese', 'spanish', 'turkish']
words_to_add = ['hevc', 'reenc', 'x265', 'danish', 'deutsch', 'flemish', 'italian',
'nordic', 'norwegian', 'portuguese', 'spanish', 'turkish']
config_words = sickbeard.IGNORE_WORDS.split(',')
new_list = []
for new_word in words_to_add:
@ -759,7 +795,8 @@ class ConfigMigrator():
sickbeard.IGNORE_WORDS = ', '.join(sorted(new_list))
def _migrate_v13(self):
@staticmethod
def _migrate_v13():
# change dereferrer.org urls to blank, but leave any other url untouched
if sickbeard.ANON_REDIRECT == 'http://dereferer.org/?':
sickbeard.ANON_REDIRECT = ''

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 = 20008
MAX_DB_VERSION = 20009
TEST_BASE_VERSION = None # the base production db version, only needed for TEST db versions (>=100000)
@ -44,12 +44,13 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_duplicate_shows(self, column='indexer_id'):
sql_results = self.connection.select(
'SELECT show_id, ' + column + ', COUNT(' + column + ') as count FROM tv_shows GROUP BY ' + column + ' HAVING count > 1')
'SELECT show_id, %(col)s, COUNT(%(col)s) as count FROM tv_shows GROUP BY %(col)s HAVING count > 1'
% {'col': column})
for cur_duplicate in sql_results:
logger.log(u'Duplicate show detected! %s: %s count: %s' % (column, cur_duplicate[column],
cur_duplicate['count']), logger.DEBUG)
logger.log(u'Duplicate show detected! %s: %s count: %s' % (
column, cur_duplicate[column], cur_duplicate['count']), logger.DEBUG)
cur_dupe_results = self.connection.select(
'SELECT show_id, ' + column + ' FROM tv_shows WHERE ' + column + ' = ? LIMIT ?',
@ -58,9 +59,8 @@ class MainSanityCheck(db.DBSanityCheck):
cl = []
for cur_dupe_id in cur_dupe_results:
logger.log(
u'Deleting duplicate show with %s: %s show_id: %s' % (column, cur_dupe_id[column],
cur_dupe_id['show_id']))
logger.log(u'Deleting duplicate show with %s: %s show_id: %s' % (
column, cur_dupe_id[column], cur_dupe_id['show_id']))
cl.append(['DELETE FROM tv_shows WHERE show_id = ?', [cur_dupe_id['show_id']]])
if 0 < len(cl):
@ -72,7 +72,10 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_duplicate_episodes(self):
sql_results = self.connection.select(
'SELECT showid, season, episode, COUNT(showid) as count FROM tv_episodes GROUP BY showid, season, episode HAVING count > 1')
'SELECT showid, season, episode, COUNT(showid) as count'
' FROM tv_episodes'
' GROUP BY showid, season, episode'
' HAVING count > 1')
for cur_duplicate in sql_results:
@ -81,7 +84,10 @@ class MainSanityCheck(db.DBSanityCheck):
cur_duplicate['count']), logger.DEBUG)
cur_dupe_results = self.connection.select(
'SELECT episode_id FROM tv_episodes WHERE showid = ? AND season = ? and episode = ? ORDER BY episode_id DESC LIMIT ?',
'SELECT episode_id'
' FROM tv_episodes'
' WHERE showid = ? AND season = ? and episode = ?'
' ORDER BY episode_id DESC LIMIT ?',
[cur_duplicate['showid'], cur_duplicate['season'], cur_duplicate['episode'],
int(cur_duplicate['count']) - 1]
)
@ -100,12 +106,15 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_orphan_episodes(self):
sql_results = self.connection.select(
'SELECT episode_id, showid, tv_shows.indexer_id FROM tv_episodes LEFT JOIN tv_shows ON tv_episodes.showid=tv_shows.indexer_id WHERE tv_shows.indexer_id is NULL')
'SELECT episode_id, showid, tv_shows.indexer_id'
' FROM tv_episodes'
' LEFT JOIN tv_shows ON tv_episodes.showid=tv_shows.indexer_id'
' WHERE tv_shows.indexer_id is NULL')
cl = []
for cur_orphan in sql_results:
logger.log(u'Orphan episode detected! episode_id: %s showid: %s' % (cur_orphan['episode_id'],
cur_orphan['showid']), logger.DEBUG)
logger.log(u'Orphan episode detected! episode_id: %s showid: %s' % (
cur_orphan['episode_id'], cur_orphan['showid']), logger.DEBUG)
logger.log(u'Deleting orphan episode with episode_id: %s' % cur_orphan['episode_id'])
cl.append(['DELETE FROM tv_episodes WHERE episode_id = ?', [cur_orphan['episode_id']]])
@ -150,15 +159,15 @@ class MainSanityCheck(db.DBSanityCheck):
sql_results = self.connection.select(
'SELECT episode_id, showid FROM tv_episodes WHERE status = ? or ( airdate > ? AND status in (?,?) ) or '
'( airdate <= 1 AND status = ? )', ['', cur_date.toordinal(), common.SKIPPED, common.WANTED, common.WANTED])
'(airdate <= 1 AND status = ? )', ['', cur_date.toordinal(), common.SKIPPED, common.WANTED, common.WANTED])
cl = []
for cur_unaired in sql_results:
logger.log(u'UNAIRED episode detected! episode_id: %s showid: %s' % (cur_unaired['episode_id'],
cur_unaired['showid']), logger.DEBUG)
logger.log(u'UNAIRED episode detected! episode_id: %s showid: %s' % (
cur_unaired['episode_id'], cur_unaired['showid']), logger.DEBUG)
logger.log(u'Fixing unaired episode status with episode_id: %s' % cur_unaired['episode_id'])
cl.append(['UPDATE tv_episodes SET status = ? WHERE episode_id = ?',
[common.UNAIRED, cur_unaired['episode_id']]])
[common.UNAIRED, cur_unaired['episode_id']]])
if 0 < len(cl):
self.connection.mass_action(cl)
@ -176,42 +185,90 @@ class MainSanityCheck(db.DBSanityCheck):
self.connection.action('UPDATE scene_exceptions SET season = -1 WHERE season = "null"')
def fix_orphan_not_found_show(self):
sql_result = self.connection.action('DELETE FROM tv_shows_not_found WHERE NOT EXISTS (SELECT NULL FROM '
'tv_shows WHERE tv_shows_not_found.indexer == tv_shows.indexer AND '
'tv_shows_not_found.indexer_id == tv_shows.indexer_id)')
sql_result = self.connection.action(
'DELETE FROM tv_shows_not_found'
' WHERE NOT EXISTS (SELECT NULL FROM tv_shows WHERE tv_shows_not_found.indexer == tv_shows.indexer AND'
' tv_shows_not_found.indexer_id == tv_shows.indexer_id)')
if sql_result.rowcount:
logger.log('Fixed orphaned not found shows')
# ======================
# = Main DB Migrations =
# ======================
# Add new migrations at the bottom of the list; subclass the previous migration.
# 0 -> 20003
class InitialSchema(db.SchemaUpgrade):
# ======================
# = Main DB Migrations =
# ======================
# Add new migrations at the bottom of the list; subclass the previous migration.
# 0 -> 20007
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasTable('tv_shows') and not self.hasTable('db_version'):
queries = [
# original sick beard tables
'CREATE TABLE db_version (db_version INTEGER);',
'CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC, quality NUMERIC, resource TEXT, provider TEXT, version NUMERIC)',
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search 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, absolute_number NUMERIC, scene_absolute_number NUMERIC, version NUMERIC, release_group TEXT, trakt_watched 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, anime NUMERIC, scene NUMERIC, overview TEXT, tag TEXT)',
'CREATE INDEX idx_showid ON tv_episodes (showid)',
'CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes (showid,airdate)',
# blacklist
'CREATE TABLE blacklist (show_id INTEGER, range TEXT, keyword TEXT)',
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC, PRIMARY KEY (indexer_id, indexer))',
'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 scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, absolute_number NUMERIC, scene_absolute_number NUMERIC, PRIMARY KEY (indexer_id, season, episode))',
'CREATE TABLE whitelist (show_id INTEGER, range TEXT, keyword TEXT)',
'CREATE TABLE xem_refresh (indexer TEXT, indexer_id INTEGER PRIMARY KEY, last_refreshed INTEGER)',
# db_version
'CREATE TABLE db_version (db_version INTEGER)',
'INSERT INTO db_version (db_version) VALUES (20008)',
# flags
'CREATE TABLE flags (flag PRIMARY KEY NOT NULL)',
# history
'CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC,'
' quality NUMERIC, resource TEXT, provider TEXT, version NUMERIC)',
# imdb_info
'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)',
# indexer_mapping
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL,'
' mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0,'
' PRIMARY KEY (indexer_id, indexer, mindexer))',
'CREATE INDEX idx_mapping ON indexer_mapping (indexer_id, indexer)',
# info
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC,'
' last_run_backlog NUMERIC NOT NULL DEFAULT 1)',
# scene_exceptions
'CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY,'
' show_name TEXT, season NUMERIC, custom NUMERIC)',
# scene_exceptions_refresh
'CREATE TABLE scene_exceptions_refresh (list TEXT PRIMARY KEY, last_refreshed INTEGER)',
# scene_numbering
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER,'
' scene_season INTEGER, scene_episode INTEGER, absolute_number NUMERIC, scene_absolute_number NUMERIC,'
' PRIMARY KEY (indexer_id, season, episode))',
# tv_episodes
'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, absolute_number NUMERIC, scene_absolute_number NUMERIC,'
' version NUMERIC, release_group TEXT, trakt_watched NUMERIC)',
'CREATE INDEX idx_showid ON tv_episodes (showid)',
'CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes (showid, airdate)',
'CREATE INDEX idx_sta_epi_air ON tv_episodes (status, episode, airdate)',
'CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season, episode, status, airdate)',
'CREATE INDEX idx_status ON tv_episodes (status, season, episode, airdate)',
# tv_episodes_watched
'CREATE TABLE tv_episodes_watched (tvep_id NUMERIC NOT NULL, clientep_id TEXT, label TEXT,'
' played NUMERIC DEFAULT 0 NOT NULL, date_watched NUMERIC NOT NULL, date_added NUMERIC,'
' status NUMERIC, location TEXT, file_size NUMERIC, hide INT default 0 not null)',
# tv_shows
'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, anime NUMERIC, scene NUMERIC, overview TEXT, tag TEXT)',
'CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id)',
'CREATE INDEX idx_sta_epi_air ON tv_episodes (status,episode, airdate)',
'CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season,episode, status, airdate)',
'CREATE INDEX idx_status ON tv_episodes (status,season,episode,airdate)',
'INSERT INTO db_version (db_version) VALUES (20003)'
# tv_shows_not_found
'CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL,'
' fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))',
# webdl_types
'CREATE TABLE webdl_types (dname TEXT NOT NULL, regex TEXT NOT NULL)',
# whitelist
'CREATE TABLE whitelist (show_id INTEGER, range TEXT, keyword TEXT)',
# xem_refresh
'CREATE TABLE xem_refresh (indexer TEXT, indexer_id INTEGER PRIMARY KEY, last_refreshed INTEGER)',
]
for query in queries:
self.connection.action(query)
@ -220,20 +277,22 @@ class InitialSchema(db.SchemaUpgrade):
cur_db_version = self.checkDBVersion()
if cur_db_version < MIN_DB_VERSION:
logger.log_error_and_exit(u'Your database version ('
+ str(cur_db_version)
+ ') is too old to migrate from what this version of SickGear supports ('
+ str(MIN_DB_VERSION) + ').' + "\n"
+ 'Upgrade using a previous version (tag) build 496 to build 501 of SickGear first or remove database file to begin fresh.'
)
logger.log_error_and_exit(
u'Your database version (' + str(cur_db_version)
+ ') is too old to migrate from what this version of SickGear supports ('
+ str(MIN_DB_VERSION) + ').' + "\n"
+ 'Upgrade using a previous version (tag) build 496 to build 501 of SickGear'
' first or remove database file to begin fresh.'
)
if cur_db_version > MAX_DB_VERSION:
logger.log_error_and_exit(u'Your database version ('
+ str(cur_db_version)
+ ') has been incremented past what this version of SickGear supports ('
+ str(MAX_DB_VERSION) + ').' + "\n"
+ 'If you have used other forks of SickGear, your database may be unusable due to their modifications.'
)
logger.log_error_and_exit(
u'Your database version (' + str(cur_db_version)
+ ') has been incremented past what this version of SickGear supports ('
+ str(MAX_DB_VERSION) + ').\n'
+ 'If you have used other forks of SickGear,'
' your database may be unusable due to their modifications.'
)
return self.checkDBVersion()
@ -270,7 +329,9 @@ class AddSizeAndSceneNameFields(db.SchemaUpgrade):
for cur_result in history_results:
# find the associated download, if there isn't one then ignore it
download_results = self.connection.select(
'SELECT resource FROM history WHERE provider = -1 AND showid = ? AND season = ? AND episode = ? AND date > ?',
'SELECT resource'
' FROM history'
' WHERE provider = -1 AND showid = ? AND season = ? AND episode = ? AND date > ?',
[cur_result['showid'], cur_result['season'], cur_result['episode'], cur_result['date']])
if not download_results:
logger.log(u'Found a snatch in the history for ' + cur_result[
@ -286,7 +347,9 @@ class AddSizeAndSceneNameFields(db.SchemaUpgrade):
# find the associated episode on disk
ep_results = self.connection.select(
'SELECT episode_id, status FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND location != ""',
'SELECT episode_id, status'
' FROM tv_episodes'
' WHERE showid = ? AND season = ? AND episode = ? AND location != ""',
[cur_result['showid'], cur_result['season'], cur_result['episode']])
if not ep_results:
logger.log(
@ -358,8 +421,14 @@ class RenameSeasonFolders(db.SchemaUpgrade):
# rename the column
self.connection.action('ALTER TABLE tv_shows RENAME TO tmp_tv_shows')
self.connection.action(
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, location TEXT, show_name TEXT, tvdb_id NUMERIC, network TEXT, genre TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, tvr_id NUMERIC, tvr_name TEXT, air_by_date NUMERIC, lang TEXT)')
sql = 'INSERT INTO tv_shows(show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, tvr_id, tvr_name, air_by_date, lang) SELECT show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, seasonfolders, paused, startyear, tvr_id, tvr_name, air_by_date, lang FROM tmp_tv_shows'
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, location TEXT, show_name TEXT, tvdb_id NUMERIC,'
' network TEXT, genre TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT,'
' flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, tvr_id NUMERIC, tvr_name TEXT,'
' air_by_date NUMERIC, lang TEXT)')
sql = 'INSERT INTO tv_shows(show_id, location, show_name, tvdb_id, network, genre, runtime,' \
' quality, airs, status, flatten_folders, paused, startyear, tvr_id, tvr_name, air_by_date, lang)' \
' SELECT show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status,' \
' seasonfolders, paused, startyear, tvr_id, tvr_name, air_by_date, lang FROM tmp_tv_shows'
self.connection.action(sql)
# flip the values to be opposite of what they were before
@ -419,13 +488,13 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
return result
def _update_composite_qualities(self, status):
'''
"""
Unpack, Update, Return new quality values
Unpack the composite archive/initial values.
Update either qualities if needed.
Then return the new compsite quality value.
'''
"""
best = (status & (0xffff << 16)) >> 16
initial = status & 0xffff
@ -449,7 +518,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
new_hd = common.Quality.combineQualities([common.Quality.HDTV, common.Quality.HDWEBDL,
common.Quality.HDBLURAY], [])
# update ANY -- shift existing qualities and add new 1080p qualities, note that rawHD was not added to the ANY template
# update ANY -- shift existing qualities and add new 1080p qualities,
# note that rawHD was not added to the ANY template
old_any = common.Quality.combineQualities(
[common.Quality.SDTV, common.Quality.SDDVD, common.Quality.HDTV, common.Quality.HDWEBDL >> 2,
common.Quality.HDBLURAY >> 3, common.Quality.UNKNOWN], [])
@ -472,7 +542,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
cl.append(['UPDATE tv_shows SET quality = ? WHERE show_id = ?', [new_quality, cur_show['show_id']]])
self.connection.mass_action(cl)
# update status that are are within the old hdwebdl (1<<3 which is 8) and better -- exclude unknown (1<<15 which is 32768)
# update status that are are within the old hdwebdl
# (1<<3 which is 8) and better -- exclude unknown (1<<15 which is 32768)
logger.log(u'[2/4] Updating the status for the episodes within each show...', logger.MESSAGE)
cl = []
episodes = self.connection.select('SELECT * FROM tv_episodes WHERE status < 3276800 AND status >= 800')
@ -481,7 +552,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
[self._update_status(cur_episode['status']), cur_episode['episode_id']]])
self.connection.mass_action(cl)
# make two seperate passes through the history since snatched and downloaded (action & quality) may not always coordinate together
# make two seperate passes through the history since snatched and downloaded (action & quality)
# may not always coordinate together
# update previous history so it shows the correct action
logger.log(u'[3/4] Updating history to reflect the correct action...', logger.MESSAGE)
@ -564,7 +636,9 @@ class AddIMDbInfo(db.SchemaUpgrade):
db.backup_database('sickbeard.db', self.checkDBVersion())
db_backed_up = True
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)')
'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')
@ -680,9 +754,18 @@ class ConvertTVShowsToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE tv_shows RENAME TO tmp_tv_shows')
self.connection.action(
'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)')
'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)')
self.connection.action(
'INSERT INTO tv_shows(show_id, indexer_id, show_name, location, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id, last_update_indexer, dvdorder) SELECT show_id, tvdb_id, show_name, location, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id, last_update_tvdb, dvdorder FROM tmp_tv_shows')
'INSERT INTO tv_shows(show_id, indexer_id, show_name, location, network, genre, runtime, quality, airs,'
' status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id,'
' last_update_indexer, dvdorder)'
' SELECT show_id, tvdb_id, show_name, location, network, genre, runtime,'
' quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list,'
' imdb_id, last_update_tvdb, dvdorder FROM tmp_tv_shows')
self.connection.action('DROP TABLE tmp_tv_shows')
self.connection.action('CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id);')
@ -707,9 +790,16 @@ class ConvertTVEpisodesToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE tv_episodes RENAME TO tmp_tv_episodes')
self.connection.action(
'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)')
'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)')
self.connection.action(
'INSERT INTO tv_episodes(episode_id, showid, indexerid, name, season, episode, description, airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount, subtitles_lastsearch, is_proper) SELECT episode_id, showid, tvdbid, name, season, episode, description, airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount, subtitles_lastsearch, is_proper FROM tmp_tv_episodes')
'INSERT INTO tv_episodes(episode_id, showid, indexerid, name, season, episode, description, airdate,'
' hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount,'
' subtitles_lastsearch, is_proper) SELECT episode_id, showid, tvdbid, name, season, episode, description,'
' airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount,'
' subtitles_lastsearch, is_proper FROM tmp_tv_episodes')
self.connection.action('DROP TABLE tmp_tv_episodes')
self.connection.action('CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes(showid,airdate);')
@ -737,9 +827,13 @@ class ConvertIMDBInfoToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE imdb_info RENAME TO tmp_imdb_info')
self.connection.action(
'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 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)')
self.connection.action(
'INSERT INTO imdb_info(indexer_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes, certificates, rating, votes, last_update) SELECT tvdb_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes, certificates, rating, votes, last_update FROM tmp_imdb_info')
'INSERT INTO imdb_info(indexer_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes,'
' certificates, rating, votes, last_update) SELECT tvdb_id, imdb_id, title, year, akas, runtimes, genres,'
' countries, country_codes, certificates, rating, votes, last_update FROM tmp_imdb_info')
self.connection.action('DROP TABLE tmp_imdb_info')
self.incDBVersion()
@ -761,7 +855,8 @@ class ConvertInfoToIndexerScheme(db.SchemaUpgrade):
self.connection.action(
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC)')
self.connection.action(
'INSERT INTO info(last_backlog, last_indexer, last_proper_search) SELECT last_backlog, last_tvdb, last_proper_search FROM tmp_info')
'INSERT INTO info(last_backlog, last_indexer, last_proper_search)'
' SELECT last_backlog, last_tvdb, last_proper_search FROM tmp_info')
self.connection.action('DROP TABLE tmp_info')
self.incDBVersion()
@ -791,7 +886,9 @@ class AddSceneNumbering(db.SchemaUpgrade):
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))')
'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()
@ -981,7 +1078,8 @@ class AddIndexerMapping(db.SchemaUpgrade):
logger.log(u'Adding table indexer_mapping')
self.connection.action(
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC, PRIMARY KEY (indexer_id, indexer))')
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))')
self.incDBVersion()
return self.checkDBVersion()
@ -1190,7 +1288,9 @@ class ChangeMapIndexer(db.SchemaUpgrade):
logger.log(u'Changing table indexer_mapping')
self.connection.action(
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL, mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (indexer_id, indexer, mindexer))')
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL,'
' mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0,'
' PRIMARY KEY (indexer_id, indexer, mindexer))')
self.connection.action('CREATE INDEX IF NOT EXISTS idx_mapping ON indexer_mapping (indexer_id, indexer)')
@ -1204,21 +1304,23 @@ class ChangeMapIndexer(db.SchemaUpgrade):
self.connection.action('CREATE TABLE scene_exceptions_refresh (list TEXT PRIMARY KEY, last_refreshed INTEGER)')
if self.hasTable('scene_exceptions'):
self.connection.action('DROP TABLE scene_exceptions')
self.connection.action('CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY, show_name TEXT, season NUMERIC, custom NUMERIC)')
self.connection.action('CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY,'
' indexer_id INTEGER KEY, show_name TEXT, season NUMERIC, custom NUMERIC)')
try:
cachedb = db.DBConnection(filename='cache.db')
if cachedb.hasTable('scene_exceptions'):
sqlResults = cachedb.action('SELECT * FROM scene_exceptions')
sql_results = cachedb.action('SELECT * FROM scene_exceptions')
cs = []
for r in sqlResults:
cs.append(['INSERT OR REPLACE INTO scene_exceptions (exception_id, indexer_id, show_name, season, custom)'
' VALUES (?,?,?,?,?)', [r['exception_id'], r['indexer_id'], r['show_name'],
r['season'], r['custom']]])
for r in sql_results:
cs.append(
['INSERT OR REPLACE INTO scene_exceptions (exception_id, indexer_id, show_name, season, custom)'
' VALUES (?,?,?,?,?)',
[r['exception_id'], r['indexer_id'], r['show_name'], r['season'], r['custom']]])
if len(cs) > 0:
if 0 < len(cs):
self.connection.mass_action(cs)
except:
except (StandardError, Exception):
pass
keep_tables = {'scene_exceptions', 'scene_exceptions_refresh', 'info', 'indexer_mapping', 'blacklist',
@ -1242,7 +1344,10 @@ class AddShowNotFoundCounter(db.SchemaUpgrade):
logger.log(u'Adding table tv_shows_not_found')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL, fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC, PRIMARY KEY (indexer_id, indexer))')
self.connection.action(
'CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL,'
' fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))')
self.setDBVersion(20005)
return self.checkDBVersion()
@ -1255,7 +1360,7 @@ class AddFlagTable(db.SchemaUpgrade):
logger.log(u'Adding table flags')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE flags (flag PRIMARY KEY NOT NULL )')
self.connection.action('CREATE TABLE flags (flag PRIMARY KEY NOT NULL)')
self.setDBVersion(20006)
return self.checkDBVersion()
@ -1275,7 +1380,29 @@ class DBIncreaseTo20007(db.SchemaUpgrade):
class AddWebdlTypesTable(db.SchemaUpgrade):
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE webdl_types (dname TEXT NOT NULL , regex TEXT NOT NULL )')
self.connection.action('CREATE TABLE webdl_types (dname TEXT NOT NULL, regex TEXT NOT NULL)')
self.setDBVersion(20008)
return self.checkDBVersion()
# 20008 -> 20009
class AddWatched(db.SchemaUpgrade):
def execute(self):
# remove old table from version 20007
if self.hasTable('tv_episodes_watched') and not self.hasColumn('tv_episodes_watched', 'clientep_id'):
self.connection.action('DROP TABLE tv_episodes_watched')
self.connection.action('VACUUM')
if not self.hasTable('tv_episodes_watched'):
logger.log(u'Adding table tv_episodes_watched')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action(
'CREATE TABLE tv_episodes_watched (tvep_id NUMERIC NOT NULL, clientep_id TEXT, label TEXT,'
' played NUMERIC DEFAULT 0 NOT NULL, date_watched NUMERIC NOT NULL, date_added NUMERIC,'
' status NUMERIC, location TEXT, file_size NUMERIC, hide INT default 0 not null)'
)
self.setDBVersion(20009)
return self.checkDBVersion()

View file

@ -47,26 +47,35 @@ def dbFilename(filename='sickbeard.db', suffix=None):
return ek.ek(os.path.join, sickbeard.DATA_DIR, filename)
def mass_upsert_sql(tableName, valueDict, keyDict):
def mass_upsert_sql(table_name, value_dict, key_dict, sanitise=True):
"""
use with cl.extend(mass_upsert_sql(tableName, valueDict, keyDict))
:param tableName: table name
:param valueDict: dict of values to be set {'table_fieldname': value}
:param keyDict: dict of restrains for update {'table_fieldname': value}
:param table_name: table name
:param value_dict: dict of values to be set {'table_fieldname': value}
:param key_dict: dict of restrains for update {'table_fieldname': value}
:param sanitise: True to remove k, v pairs in keyDict from valueDict as they must not exist in both.
This option has a performance hit so it's best to remove key_dict keys from value_dict and set this False instead.
:type sanitise: Boolean
:return: list of 2 sql command
"""
cl = []
genParams = lambda myDict: [x + ' = ?' for x in myDict.keys()]
gen_params = (lambda my_dict: [x + ' = ?' for x in my_dict.keys()])
cl.append(['UPDATE [%s] SET %s WHERE %s' % (
tableName, ', '.join(genParams(valueDict)), ' AND '.join(genParams(keyDict))), valueDict.values() + keyDict.values()])
# sanity: remove k, v pairs in keyDict from valueDict
if sanitise:
value_dict = dict(filter(lambda (k, _): k not in key_dict.keys(), value_dict.items()))
cl.append(['UPDATE [%s] SET %s WHERE %s' %
(table_name, ', '.join(gen_params(value_dict)), ' AND '.join(gen_params(key_dict))),
value_dict.values() + key_dict.values()])
cl.append(['INSERT INTO [' + tableName + '] (' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.keys() + keyDict.keys()]) + ')' +
' SELECT ' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.values() + keyDict.values()]) + ' WHERE changes() = 0'])
cl.append(['INSERT INTO [' + table_name + '] (' +
', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in value_dict.keys() + key_dict.keys()]) + ')' +
' SELECT ' +
', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in value_dict.values() + key_dict.values()]) +
' WHERE changes() = 0'])
return cl
@ -261,12 +270,41 @@ class DBConnection(object):
return False
def add_flag(self, flag_name):
if not self.has_flag(flag_name):
has_flag = self.has_flag(flag_name)
if not has_flag:
self.action('INSERT INTO flags (flag) VALUES (?)', [flag_name])
return not has_flag
def remove_flag(self, flag_name):
if self.has_flag(flag_name):
has_flag = self.has_flag(flag_name)
if has_flag:
self.action('DELETE FROM flags WHERE flag = ?', [flag_name])
return has_flag
def toggle_flag(self, flag_name):
"""
Add or remove a flag
:param flag_name: Name of flag
:type flag_name: String
:return: True if this call added the flag, False if flag is removed
:rtype: Boolean
"""
if self.remove_flag(flag_name):
return False
self.add_flag(flag_name)
return True
def set_flag(self, flag_name, state=True):
"""
Set state of flag
:param flag_name: Name of flag
:type flag_name: String
:param state: If true, create flag otherwise remove flag
:type state: Boolean
:return: Previous state of flag
:rtype: Boolean
"""
return (self.add_flag, self.remove_flag)[not bool(state)](flag_name)
def close(self):
"""Close database connection"""
@ -515,6 +553,7 @@ def MigrationCode(myDB):
20005: sickbeard.mainDB.AddFlagTable,
20006: sickbeard.mainDB.DBIncreaseTo20007,
20007: sickbeard.mainDB.AddWebdlTypesTable,
20008: sickbeard.mainDB.AddWatched,
# 20002: sickbeard.mainDB.AddCoolSickGearFeature3,
}
@ -532,6 +571,9 @@ def MigrationCode(myDB):
else:
while db_version < sickbeard.mainDB.MAX_DB_VERSION:
if None is schema[db_version]: # skip placeholders used when multi PRs are updating DB
db_version += 1
continue
try:
update = schema[db_version](myDB)
db_version = update.execute()

View file

@ -1618,3 +1618,28 @@ def freespace(path=None):
pass
return result
def path_mapper(search, replace, subject):
"""
Substitute strings in a path
:param search: Search text
:type search: String
:param replace: Replacement text
:type replace: String
:param subject: Path text to search
:type subject: String
:return: Subject with or without substitution, True if a change was made otherwise False
:rtype: Tuple
"""
delim = '/!~!/'
search = re.sub(r'[\\]', delim, search)
replace = re.sub(r'[\\]', delim, replace)
path = re.sub(r'[\\]', delim, subject)
result = re.sub('(?i)^%s' % search, replace, path)
if re.search(delim, path):
result = os.path.normpath(re.sub(delim, '/', result))
return result, result != subject

View file

@ -125,6 +125,11 @@ class indexerApi(object):
def indexers(self):
return dict((int(x['id']), x['name']) for x in indexerConfig.values() if not x['mapped_only'])
@property
def search_indexers(self):
return dict((int(x['id']), x['name']) for x in indexerConfig.values() if not x['mapped_only'] and
x.get('active') and not x.get('defunct'))
@property
def all_indexers(self):
"""

View file

@ -54,6 +54,21 @@ normal_regexes = [
'''
),
('non_standard_multi_ep',
# Show Name - S01E02&03 - My Ep Name
# Show Name - S01E02and03 - My Ep Name
'''
^((?P<series_name>.+?)[. _-]+)? # Show_Name and separator
s(?P<season_num>\d+)[. _-]* # S01 and optional separator
e(?P<ep_num>\d+) # E02 and separator
(([. _-]*and|&|to) # linking and/&/to
(?P<extra_ep_num>(?!(2160|1080|720|480)[pi])\d+))+ # additional E03/etc
[. _-]*((?P<extra_info>.+?) # Source_Quality_Etc-
((?<![. _-])(?<!WEB) # Make sure this is really the release group
-(?P<release_group>[^- ]+))?)?$ # Group
'''
),
('standard',
# Show.Name.S01E02.Source.Quality.Etc-Group
# Show Name - S01E02 - My Ep Name
@ -73,6 +88,22 @@ normal_regexes = [
'''
),
('fov_non_standard_multi_ep',
# Show Name - 1x02and03and04 - My Ep Name
'''
^((?P<series_name>.+?)[\[. _-]+)? # Show_Name and separator
(?P<season_num>\d+)x # 1x
(?P<ep_num>\d+) # 02 and separator
(([. _-]*and|&|to) # linking x/- char
(?P<extra_ep_num>
(?!(2160|1080|720|480)[pi])(?!(?<=x)264) # ignore obviously wrong multi-eps
\d+))+ # additional x03/etc
[\]. _-]*((?P<extra_info>.+?) # Source_Quality_Etc-
((?<![. _-])(?<!WEB) # Make sure this is really the release group
-(?P<release_group>[^- ]+))?)?$ # Group
'''
),
('fov',
# Show_Name.1x02.Source_Quality_Etc-Group
# Show Name - 1x02 - My Ep Name

View file

@ -295,31 +295,35 @@ def load_network_dict(load=True):
# get timezone of a network or return default timezone
def get_network_timezone(network):
def get_network_timezone(network, return_name=False):
if network is None:
return sb_timezone
timezone = None
timezone_name = None
try:
if zoneinfo.ZONEFILENAME is not None:
if not network_dict:
load_network_dict()
try:
timezone = tz.gettz(network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower()),
zoneinfo_priority=True)
except:
timezone_name = network_dupes.get(network) or network_dict.get(network.replace(' ', '').lower())
timezone = tz.gettz(timezone_name, zoneinfo_priority=True)
except (StandardError, Exception):
pass
if timezone is None:
cc = re.search(r'\(([a-z]+)\)$', network, flags=re.I)
try:
timezone = tz.gettz(country_timezones.get(cc.group(1).upper()), zoneinfo_priority=True)
except:
timezone_name = country_timezones.get(cc.group(1).upper())
timezone = tz.gettz(timezone_name, zoneinfo_priority=True)
except (StandardError, Exception):
pass
except:
except (StandardError, Exception):
pass
if return_name:
return timezone if isinstance(timezone, datetime.tzinfo) else sb_timezone, timezone_name
return timezone if isinstance(timezone, datetime.tzinfo) else sb_timezone

View file

@ -122,7 +122,7 @@ class EmbyNotifier(Notifier):
else:
new_keys += [key]
apikeys = (new_keys, [x.strip() for x in sickbeard.EMBY_APIKEY.split(',') if x.strip()] + new_keys)[has_old_key]
apikeys = has_old_key and [x.strip() for x in sickbeard.EMBY_APIKEY.split(',') if x.strip()] or [] + new_keys
if len(hosts) != len(apikeys):
message = ('Not enough Api keys for hosts', 'More Api keys than hosts')[len(apikeys) > len(hosts)]
@ -178,5 +178,10 @@ class EmbyNotifier(Notifier):
def discover_server(self):
return self._discover_server()
def check_config(self, hosts=None, apikeys=None):
self._testing = True # ensure _choose() uses passed args
return self._check_config(hosts, apikeys)
notifier = EmbyNotifier

View file

@ -58,10 +58,11 @@ except ImportError:
class ProcessTVShow(object):
""" Process a TV Show """
def __init__(self, webhandler=None, is_basedir=True):
def __init__(self, webhandler=None, is_basedir=True, skip_failure_processing=False):
self.files_passed = 0
self.files_failed = 0
self.fail_detected = False
self.skip_failure_processing = skip_failure_processing
self._output = []
self.webhandler = webhandler
self.is_basedir = is_basedir
@ -919,6 +920,10 @@ class ProcessTVShow(object):
def _process_failed(self, dir_name, nzb_name, showObj=None):
""" Process a download that did not complete correctly """
if self.skip_failure_processing:
self._log_helper('Download was not added by SickGear, ignoring failure', logger.WARNING)
return
if sickbeard.USE_FAILED_DOWNLOADS:
processor = None
@ -947,8 +952,9 @@ class ProcessTVShow(object):
# backward compatibility prevents the case of this function name from being updated to PEP8
def processDir(dir_name, nzb_name=None, process_method=None, force=False, force_replace=None,
failed=False, type='auto', cleanup=False, webhandler=None, showObj=None, is_basedir=True):
failed=False, type='auto', cleanup=False, webhandler=None, showObj=None, is_basedir=True,
skip_failure_processing=False):
# backward compatibility prevents the case of this function name from being updated to PEP8
return ProcessTVShow(webhandler, is_basedir).process_dir(
return ProcessTVShow(webhandler, is_basedir, skip_failure_processing=skip_failure_processing).process_dir(
dir_name, nzb_name, process_method, force, force_replace, failed, type, cleanup, showObj)

View file

@ -29,7 +29,7 @@ from . import newznab, omgwtfnzbs
from . import alpharatio, beyondhd, bithdtv, bitmetv, blutopia, btn, btscene, dh, ettv, \
fano, filelist, funfile, gftracker, grabtheinfo, hdbits, hdspace, hdtorrents, \
iptorrents, limetorrents, magnetdl, morethan, nebulance, ncore, nyaa, pisexy, potuk, pretome, privatehd, ptf, \
rarbg, revtt, scenehd, scenetime, shazbat, skytorrents, speedcd, \
rarbg, revtt, scenehd, scenetime, shazbat, showrss, skytorrents, speedcd, \
thepiratebay, torlock, torrentbytes, torrentday, torrenting, torrentleech, \
torrentz2, tvchaosuk, wop, zooqle
# anime
@ -77,6 +77,7 @@ __all__ = ['omgwtfnzbs',
'scenehd',
'scenetime',
'shazbat',
'showrss',
'skytorrents',
'speedcd',
'thepiratebay',

View file

@ -100,7 +100,8 @@ class ProviderFailList(object):
fail_hour = e.fail_time.time().hour
date_time = datetime.datetime.combine(fail_date, datetime.time(hour=fail_hour))
if ProviderFailTypes.names[e.fail_type] not in fail_dict.get(date_time, {}):
default = {'date': str(fail_date), 'date_time': date_time, 'multirow': False}
default = {'date': str(fail_date), 'date_time': date_time,
'timestamp': helpers.tryInt(sbdatetime.totimestamp(e.fail_time)), 'multirow': False}
for et in ProviderFailTypes.names.itervalues():
default[et] = b_d.copy()
fail_dict.setdefault(date_time, default)[ProviderFailTypes.names[e.fail_type]]['count'] = 1
@ -502,8 +503,10 @@ class GenericProvider(object):
kwargs['raise_exceptions'] = True
kwargs['raise_status_code'] = True
for k, v in dict(headers=self.headers, hooks=dict(response=self.cb_response), session=self.session).items():
for k, v in dict(headers=self.headers, hooks=dict(response=self.cb_response)).items():
kwargs.setdefault(k, v)
if 'nzbs.in' not in url: # this provider returns 503's 3 out of 4 requests with the persistent session system
kwargs.setdefault('session', self.session)
post_data = kwargs.get('post_data')
post_json = kwargs.get('post_json')

View file

@ -450,7 +450,8 @@ class NewznabProvider(generic.NZBProvider):
r_found = True
while r_found:
r_found = False
for pattern, repl in ((r'(?i)-Obfuscated$', ''), (r'(?i)-postbot$', ''), (r'(?i)[-.]English$', '')):
for pattern, repl in ((r'(?i)-Scrambled$', ''), (r'(?i)-BUYMORE$', ''), (r'(?i)-Obfuscated$', ''),
(r'(?i)-postbot$', ''), (r'(?i)[-.]English$', '')):
if re.search(pattern, title):
r_found = True
title = re.sub(pattern, repl, title)

View file

@ -0,0 +1,153 @@
# coding=utf-8
#
# Author: SickGear
#
# 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 re
import traceback
from . import generic
from sickbeard import logger
from sickbeard.bs4_parser import BS4Parser
from sickbeard.helpers import tryInt, sanitizeSceneName
from lib.unidecode import unidecode
from six.moves.html_parser import HTMLParser
class ShowRSSProvider(generic.TorrentProvider):
def __init__(self):
generic.TorrentProvider.__init__(self, 'showRSS')
self.url_base = 'https://showrss.info/'
self.urls = {'config_provider_home_uri': self.url_base,
'login_action': self.url_base + 'login',
'browse': self.url_base + 'browse/all',
'search': self.url_base + 'browse/%s'}
self.url = self.urls['config_provider_home_uri']
self.username, self.password, self.shows = 3 * [None]
def _authorised(self, **kwargs):
return super(ShowRSSProvider, self)._authorised(logged_in=(lambda y=None: self.logged_in(y)))
def logged_in(self, y):
if all([None is y or 'logout' in y,
bool(filter(lambda c: 'remember_web_' in c, self.session.cookies.keys()))]):
if None is not y:
self.shows = dict(re.findall('<option value="(\d+)">(.*?)</option>', y))
h = HTMLParser()
for k, v in self.shows.items():
self.shows[k] = sanitizeSceneName(h.unescape(unidecode(v.decode('utf-8'))))
return True
return False
def _search_provider(self, search_params, **kwargs):
results = []
if not self._authorised():
return results
items = {'Cache': [], 'Season': [], 'Episode': [], 'Propers': []}
rc = dict((k, re.compile('(?i)' + v)) for (k, v) in {'get': 'magnet'}.items())
urls = []
for mode in search_params.keys():
for search_string in search_params[mode]:
if 'Cache' == mode:
search_url = self.urls['browse']
else:
search_string = isinstance(search_string, unicode) and unidecode(search_string) or search_string
show_name = filter(lambda x: x.lower() == re.sub('\s.*', '', search_string.lower()),
self.shows.values())
if not show_name:
continue
search_url = self.urls['search'] % self.shows.keys()[self.shows.values().index(show_name[0])]
if search_url in urls:
continue
urls += [search_url]
html = self.get_url(search_url)
if self.should_skip():
return results
cnt = len(items[mode])
try:
if not html or self._has_no_results(html):
raise generic.HaltParseException
with BS4Parser(html, features=['html5lib', 'permissive']) as soup:
torrent_rows = soup.select('ul.user-timeline > li')
if not len(torrent_rows):
raise generic.HaltParseException
for tr in torrent_rows:
try:
anchor = tr.find('a', href=rc['get'])
title = self.regulate_title(anchor)
download_url = self._link(anchor['href'])
except (AttributeError, TypeError, ValueError):
continue
if title and download_url:
items[mode].append((title, download_url, None, None))
except generic.HaltParseException:
pass
except (StandardError, Exception):
logger.log(u'Failed to parse. Traceback: %s' % traceback.format_exc(), logger.ERROR)
self._log_search(mode, len(items[mode]) - cnt, search_url)
results = self._sort_seeding(mode, results + items[mode])
return results
@staticmethod
def regulate_title(anchor):
title = ''
t1 = anchor.attrs.get('title').strip()
t2 = anchor.get_text().strip()
diff, x, offset = 0, 0, 0
for x, c in enumerate(t2):
if c.lower() == t1[x-offset].lower():
title += t1[x-offset]
diff = 0
elif ' ' != c and ' ' == t1[x-offset]:
title += c
diff = 0
if ' ' == t2[x+1]:
offset += 1
else:
diff += 1
if 1 < diff:
break
return '%s%s' % (title, re.sub('(?i)(xvid|divx|[hx].?26[45])\s(\w+)$', r'\1-\2',
''.join(t1[x - (offset + diff)::]).strip()))
@staticmethod
def ui_string(key):
return ('showrss_tip' == key
and 'lists are not needed, the SickGear list is used as usual' or '')
provider = ShowRSSProvider()

View file

@ -113,10 +113,10 @@ class sbdatetime(datetime.datetime):
'july', 'august', 'september', 'october', 'november', 'december'])
@static_or_instance
def convert_to_setting(self, dt=None):
def convert_to_setting(self, dt=None, force_local=False):
obj = (dt, self)[self is not None]
try:
if 'local' == sickbeard.TIMEZONE_DISPLAY:
if force_local or 'local' == sickbeard.TIMEZONE_DISPLAY:
return obj.astimezone(sb_timezone)
except (StandardError, Exception):
pass

View file

@ -694,3 +694,66 @@ def fix_xem_numbering(indexer_id, indexer):
if 0 < len(cl):
my_db = db.DBConnection()
my_db.mass_action(cl)
def set_scene_numbering_helper(indexerid, indexer, forSeason=None, forEpisode=None, forAbsolute=None,
sceneSeason=None, sceneEpisode=None, sceneAbsolute=None):
# sanitize:
indexerid = None if indexerid in [None, 'null', ''] else int(indexerid)
indexer = None if indexer in [None, 'null', ''] else int(indexer)
show_obj = sickbeard.helpers.find_show_by_id(sickbeard.showList, {indexer: indexerid}, no_mapped_ids=True)
if not show_obj:
result = {'success': False}
return result
if not show_obj.is_anime:
for_season = None if forSeason in [None, 'null', ''] else int(forSeason)
for_episode = None if forEpisode in [None, 'null', ''] else int(forEpisode)
scene_season = None if sceneSeason in [None, 'null', ''] else int(sceneSeason)
scene_episode = None if sceneEpisode in [None, 'null', ''] else int(sceneEpisode)
action_log = u'Set episode scene numbering to %sx%s for episode %sx%s of "%s"' \
% (scene_season, scene_episode, for_season, for_episode, show_obj.name)
ep_args = {'show': indexerid, 'season': for_season, 'episode': for_episode}
scene_args = {'indexer_id': indexerid, 'indexer': indexer, 'season': for_season, 'episode': for_episode,
'sceneSeason': scene_season, 'sceneEpisode': scene_episode}
result = {'forSeason': for_season, 'forEpisode': for_episode, 'sceneSeason': None, 'sceneEpisode': None}
else:
for_absolute = None if forAbsolute in [None, 'null', ''] else int(forAbsolute)
scene_absolute = None if sceneAbsolute in [None, 'null', ''] else int(sceneAbsolute)
action_log = u'Set absolute scene numbering to %s for episode %s of "%s"' \
% (scene_absolute, for_absolute, show_obj.name)
ep_args = {'show': indexerid, 'absolute': for_absolute}
scene_args = {'indexer_id': indexerid, 'indexer': indexer, 'absolute_number': for_absolute,
'sceneAbsolute': scene_absolute}
result = {'forAbsolute': for_absolute, 'sceneAbsolute': None}
if ep_args.get('absolute'):
ep_obj = show_obj.getEpisode(absolute_number=int(ep_args['absolute']))
elif None is not ep_args['season'] and None is not ep_args['episode']:
ep_obj = show_obj.getEpisode(int(ep_args['season']), int(ep_args['episode']))
else:
ep_obj = 'Invalid paramaters'
if ep_obj is None:
ep_obj = "Episode couldn't be retrieved"
result['success'] = not isinstance(ep_obj, str)
if result['success']:
logger.log(action_log, logger.DEBUG)
set_scene_numbering(**scene_args)
show_obj.flushEpisodes()
else:
result['errorMessage'] = ep_obj
if not show_obj.is_anime:
scene_numbering = get_scene_numbering(indexerid, indexer, for_season, for_episode)
if scene_numbering:
(result['sceneSeason'], result['sceneEpisode']) = scene_numbering
else:
scene_numbering = get_scene_absolute_numbering(indexerid, indexer, for_absolute)
if scene_numbering:
result['sceneAbsolute'] = scene_numbering
return result

View file

@ -55,14 +55,13 @@ class Scheduler(threading.Thread):
self._stop.set()
self.unpause()
def check_paused(self):
if hasattr(self.action, 'check_paused'):
if self.action.check_paused():
def set_paused_state(self):
if hasattr(self.action, 'is_enabled'):
self.silent = not self.action.is_enabled()
if self.silent:
self.pause()
self.silent = True
else:
self.unpause()
self.silent = False
def timeLeft(self):
return self.cycleTime - (datetime.datetime.now() - self.lastRun)
@ -74,55 +73,60 @@ class Scheduler(threading.Thread):
return False
def run(self):
self.check_paused()
self.set_paused_state()
# if self._unpause Event() is NOT set the loop pauses
while self._unpause.wait() and not self._stop.is_set():
try:
current_time = datetime.datetime.now()
should_run = False
# check if interval has passed
if current_time - self.lastRun >= self.cycleTime:
# check if wanting to start around certain time taking interval into account
if self.start_time:
hour_diff = current_time.time().hour - self.start_time.hour
if not hour_diff < 0 and hour_diff < self.cycleTime.seconds / 3600:
should_run = True
else:
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
else:
should_run = True
if self.force:
should_run = True
if should_run and self.prevent_cycle_run is not None and self.prevent_cycle_run():
logger.log(u'%s skipping this cycleTime' % self.name, logger.WARNING)
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
if getattr(self.action, 'is_enabled', True):
try:
current_time = datetime.datetime.now()
should_run = False
if should_run:
self.lastRun = current_time
# check if interval has passed
if current_time - self.lastRun >= self.cycleTime:
# check if wanting to start around certain time taking interval into account
if self.start_time:
hour_diff = current_time.time().hour - self.start_time.hour
if not hour_diff < 0 and hour_diff < self.cycleTime.seconds / 3600:
should_run = True
else:
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
else:
should_run = True
try:
if not self.silent:
logger.log(u"Starting new thread: " + self.name, logger.DEBUG)
if self.force:
should_run = True
self.action.run()
except Exception as e:
logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR)
logger.log(repr(traceback.format_exc()), logger.ERROR)
if should_run and ((self.prevent_cycle_run is not None and self.prevent_cycle_run()) or
getattr(self.action, 'prevent_run', False)):
logger.log(u'%s skipping this cycleTime' % self.name, logger.WARNING)
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
should_run = False
finally:
if self.force:
self.force = False
if should_run:
self.lastRun = current_time
try:
if not self.silent:
logger.log(u"Starting new thread: " + self.name, logger.DEBUG)
self.action.run()
except Exception as e:
logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR)
logger.log(repr(traceback.format_exc()), logger.ERROR)
finally:
if self.force:
self.force = False
else:
# disabled schedulers will only be rechecked every 30 seconds until enabled
time.sleep(30)
time.sleep(1)
# exiting thread
self._stop.clear()
self._unpause.clear()
self._unpause.clear()

View file

@ -28,10 +28,8 @@ class ProperSearcher:
self.amActive = False
@staticmethod
def check_paused():
if sickbeard.DOWNLOAD_PROPERS:
return False
return True
def is_enabled():
return sickbeard.DOWNLOAD_PROPERS
def run(self):

View file

@ -49,10 +49,10 @@ def sortedServiceList():
newList.append(curServiceDict)
return newList
def getEnabledServiceList():
return [x['name'] for x in sortedServiceList() if x['enabled']]
def isValidLanguage(language):
return subliminal.language.language_list(language)
@ -81,18 +81,26 @@ def subtitlesLanguages(video_path):
def subtitleLanguageFilter():
return [language for language in subliminal.language.LANGUAGES if language[2] != ""]
class SubtitlesFinder():
class SubtitlesFinder:
"""
The SubtitlesFinder will be executed every hour but will not necessarly search
and download subtitles. Only if the defined rule is true
"""
@staticmethod
def check_paused():
if sickbeard.USE_SUBTITLES:
return False
return True
def __init__(self):
self.amActive = False
def run(self, force=False):
@staticmethod
def is_enabled():
return sickbeard.USE_SUBTITLES
def run(self):
if self.is_enabled():
self.amActive = True
self._main()
self.amActive = False
def _main(self):
if len(sickbeard.subtitles.getEnabledServiceList()) < 1:
logger.log(u'Not enough services selected. At least 1 service is required to search subtitles in the background', logger.ERROR)
return
@ -100,7 +108,7 @@ class SubtitlesFinder():
logger.log(u'Checking for subtitles', logger.MESSAGE)
# get episodes on which we want subtitles
# criteria is:
# criteria is:
# - show subtitles = 1
# - episode subtitles != config wanted languages or SINGLE (depends on config multi)
# - search count < 2 and diff(airdate, now) > 1 week : now -> 1d
@ -114,7 +122,7 @@ class SubtitlesFinder():
if len(sqlResults) == 0:
logger.log('No subtitles to download', logger.MESSAGE)
return
rules = self._getRules()
now = datetime.datetime.now()
for epToSub in sqlResults:
@ -122,26 +130,26 @@ class SubtitlesFinder():
if not ek.ek(os.path.isfile, epToSub['location']):
logger.log('Episode file does not exist, cannot download subtitles for episode %dx%d of show %s' % (epToSub['season'], epToSub['episode'], epToSub['show_name']), logger.DEBUG)
continue
# Old shows rule
throwaway = datetime.datetime.strptime('20110101', '%Y%m%d')
if ((epToSub['airdate_daydiff'] > 7 and epToSub['searchcount'] < 2 and now - datetime.datetime.strptime(epToSub['lastsearch'], '%Y-%m-%d %H:%M:%S') > datetime.timedelta(hours=rules['old'][epToSub['searchcount']])) or
# Recent shows rule
# Recent shows rule
(epToSub['airdate_daydiff'] <= 7 and epToSub['searchcount'] < 7 and now - datetime.datetime.strptime(epToSub['lastsearch'], '%Y-%m-%d %H:%M:%S') > datetime.timedelta(hours=rules['new'][epToSub['searchcount']]))):
logger.log('Downloading subtitles for episode %dx%d of show %s' % (epToSub['season'], epToSub['episode'], epToSub['show_name']), logger.DEBUG)
showObj = helpers.findCertainShow(sickbeard.showList, int(epToSub['showid']))
if not showObj:
logger.log(u'Show not found', logger.DEBUG)
return
epObj = showObj.getEpisode(int(epToSub["season"]), int(epToSub["episode"]))
if isinstance(epObj, str):
logger.log(u'Episode not found', logger.DEBUG)
return
previous_subtitles = epObj.subtitles
try:
subtitles = epObj.downloadSubtitles()
except:

60
sickbeard/watchedstate.py Normal file
View file

@ -0,0 +1,60 @@
#
# 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 threading
import sickbeard
from sickbeard import watchedstate_queue
class WatchedStateUpdater(object):
def __init__(self, name, queue_item):
self.amActive = False
self.lock = threading.Lock()
self.name = name
self.queue_item = queue_item
@property
def prevent_run(self):
return sickbeard.watchedStateQueueScheduler.action.is_in_queue(self.queue_item)
def run(self):
if self.is_enabled():
self.amActive = True
new_item = self.queue_item()
sickbeard.watchedStateQueueScheduler.action.add_item(new_item)
self.amActive = False
class EmbyWatchedStateUpdater(WatchedStateUpdater):
def __init__(self):
super(EmbyWatchedStateUpdater, self).__init__('Emby', watchedstate_queue.EmbyWatchedStateQueueItem)
@staticmethod
def is_enabled():
return sickbeard.USE_EMBY and sickbeard.EMBY_WATCHEDSTATE_SCHEDULED
class PlexWatchedStateUpdater(WatchedStateUpdater):
def __init__(self):
super(PlexWatchedStateUpdater, self).__init__('Plex', watchedstate_queue.PlexWatchedStateQueueItem)
@staticmethod
def is_enabled():
return sickbeard.USE_PLEX and sickbeard.PLEX_WATCHEDSTATE_SCHEDULED

View file

@ -0,0 +1,83 @@
#
# 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/>.
from __future__ import with_statement
from sickbeard import generic_queue, logger
from sickbeard.webserve import History
EMBYWATCHEDSTATE = 10
PLEXWATCHEDSTATE = 20
class WatchedStateQueue(generic_queue.GenericQueue):
def __init__(self):
super(WatchedStateQueue, self).__init__()
# self.queue_name = 'WATCHEDSTATEQUEUE'
self.queue_name = 'Q'
def is_in_queue(self, itemtype):
with self.lock:
for cur_item in self.queue + [self.currentItem]:
if isinstance(cur_item, itemtype):
return True
return False
# method for possible UI usage, can be removed if not used
def queue_length(self):
length = {'emby': 0, 'plex': 0}
with self.lock:
for cur_item in [self.currentItem] + self.queue:
if isinstance(cur_item, EmbyWatchedStateQueueItem):
length['emby'] += 1
elif isinstance(cur_item, PlexWatchedStateQueueItem):
length['plex'] += 1
return length
def add_item(self, item):
if isinstance(item, EmbyWatchedStateQueueItem) and not self.is_in_queue(EmbyWatchedStateQueueItem):
# emby watched state item
generic_queue.GenericQueue.add_item(self, item)
elif isinstance(item, PlexWatchedStateQueueItem) and not self.is_in_queue(PlexWatchedStateQueueItem):
# plex watched state item
generic_queue.GenericQueue.add_item(self, item)
else:
logger.log(u'Not adding item, it\'s already in the queue', logger.DEBUG)
class EmbyWatchedStateQueueItem(generic_queue.QueueItem):
def __init__(self):
super(EmbyWatchedStateQueueItem, self).__init__('Emby Watched', EMBYWATCHEDSTATE)
def run(self):
super(EmbyWatchedStateQueueItem, self).run()
try:
History.update_watched_state_emby()
finally:
self.finish()
class PlexWatchedStateQueueItem(generic_queue.QueueItem):
def __init__(self):
super(PlexWatchedStateQueueItem, self).__init__('Plex Watched', PLEXWATCHEDSTATE)
def run(self):
super(PlexWatchedStateQueueItem, self).run()
try:
History.update_watched_state_plex()
finally:
self.finish()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -93,6 +93,10 @@ class WebServer(threading.Thread):
# javascript
(r'%s/js/(.*)' % self.options['web_root'], webserve.BaseStaticFileHandler,
{'path': os.path.join(self.options['data_root'], 'js')}),
(r'%s/kodi/(.*)' % self.options['web_root'], webserve.RepoHandler,
{'path': os.path.join(sickbeard.CACHE_DIR, 'clients', 'kodi'),
'default_filename': 'index.html'}),
])
# Main Handler

View file

@ -43,6 +43,13 @@ simple_test_cases = {
parser.ParseResult(None, 'Show Name', 1, [2], 'Source.Quality.Etc', 'Group'),
},
'non_standard_multi_ep': {
'Show Name - S01E02and03 - My Ep Name': parser.ParseResult(None, 'Show Name', 1, [2, 3], 'My Ep Name'),
'Show Name - S01E02and03and04 - My Ep Name': parser.ParseResult(None, 'Show Name', 1, [2, 3, 4], 'My Ep Name'),
'Show Name - S01E02to03 - My Ep Name': parser.ParseResult(None, 'Show Name', 1, [2, 3], 'My Ep Name'),
'Show Name - S01E02&3&4 - My Ep Name': parser.ParseResult(None, 'Show Name', 1, [2, 3, 4], 'My Ep Name'),
},
'fov': {
'Show_Name.1x02.Source_Quality_Etc-Group':
parser.ParseResult(None, 'Show Name', 1, [2], 'Source_Quality_Etc', 'Group'),
@ -59,6 +66,11 @@ simple_test_cases = {
'Show.Name.1x02.WEB-DL': parser.ParseResult(None, 'Show Name', 1, [2], 'WEB-DL'),
},
'fov_non_standard_multi_ep': {
'Show_Name.1x02and03and04.Source_Quality_Etc-Group':
parser.ParseResult(None, 'Show Name', 1, [2, 3, 4], 'Source_Quality_Etc', 'Group'),
},
'standard_repeat': {
'Show.Name.S01E02.S01E03.Source.Quality.Etc-Group':
parser.ParseResult(None, 'Show Name', 1, [2, 3], 'Source.Quality.Etc', 'Group'),
@ -429,7 +441,7 @@ class BasicTests(test.SickbeardTestDBCase):
try:
# self.assertEqual(test_result.which_regex, [section])
self.assertEqual(test_result, result)
except:
except(StandardError, Exception):
print('air_by_date:', test_result.is_air_by_date, 'air_date:', test_result.air_date)
print('anime:', test_result.is_anime, 'ab_episode_numbers:', test_result.ab_episode_numbers)
print(test_result)
@ -444,6 +456,10 @@ class BasicTests(test.SickbeardTestDBCase):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'standard_repeat')
def test_non_standard_multi_ep_names(self):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'non_standard_multi_ep')
def test_fov_names(self):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'fov')
@ -452,6 +468,10 @@ class BasicTests(test.SickbeardTestDBCase):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'fov_repeat')
def test_fov_non_standard_multi_ep_names(self):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'fov_non_standard_multi_ep')
def test_bare_names(self):
np = parser.NameParser(False, testing=True)
self._test_names(np, 'bare')