+ The legacy SickBeard API is limited to shows from thetvdb.com.
Use the SickGear API endpoint for full access
used to give 3rd party programs limited access to SickGear
diff --git a/gui/slick/js/apibuilder.js b/gui/slick/js/apibuilder.js
index 8f7afdc3..527fb26f 100644
--- a/gui/slick/js/apibuilder.js
+++ b/gui/slick/js/apibuilder.js
@@ -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 + '
';
$('#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 + "
";
+ var html = sbRoot + "/api/" + apikey + "/" + L1 + L2 + L3 + L4 + L5 + L6 + L7 + L8 + GlobalOptions + "
";
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) {
}
}
}
- }
+ }
}
// ------
diff --git a/sickbeard/indexers/indexer_api.py b/sickbeard/indexers/indexer_api.py
index b1057514..8db5af17 100644
--- a/sickbeard/indexers/indexer_api.py
+++ b/sickbeard/indexers/indexer_api.py
@@ -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):
"""
diff --git a/sickbeard/network_timezones.py b/sickbeard/network_timezones.py
index a5076eb8..0972b311 100644
--- a/sickbeard/network_timezones.py
+++ b/sickbeard/network_timezones.py
@@ -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
diff --git a/sickbeard/sbdatetime.py b/sickbeard/sbdatetime.py
index 34bdab13..3566e64c 100644
--- a/sickbeard/sbdatetime.py
+++ b/sickbeard/sbdatetime.py
@@ -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
diff --git a/sickbeard/scene_numbering.py b/sickbeard/scene_numbering.py
index f4a8988b..4a2da326 100644
--- a/sickbeard/scene_numbering.py
+++ b/sickbeard/scene_numbering.py
@@ -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
\ No newline at end of file
diff --git a/sickbeard/show_queue.py b/sickbeard/show_queue.py
index 4110a3d8..d7500bae 100644
--- a/sickbeard/show_queue.py
+++ b/sickbeard/show_queue.py
@@ -177,10 +177,10 @@ class ShowQueue(generic_queue.GenericQueue):
def addShow(self, indexer, indexer_id, showDir, default_status=None, quality=None, flatten_folders=None,
lang='en', subtitles=None, anime=None, scene=None, paused=None, blacklist=None, whitelist=None,
- wanted_begin=None, wanted_latest=None, tag=None, new_show=False, show_name=None):
+ wanted_begin=None, wanted_latest=None, tag=None, new_show=False, show_name=None, upgrade_once=False):
queueItemObj = QueueItemAdd(indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang,
subtitles, anime, scene, paused, blacklist, whitelist,
- wanted_begin, wanted_latest, tag, new_show=new_show, show_name=show_name)
+ wanted_begin, wanted_latest, tag, new_show=new_show, show_name=show_name, upgrade_once=upgrade_once)
self.add_item(queueItemObj)
@@ -238,7 +238,7 @@ class ShowQueueItem(generic_queue.QueueItem):
class QueueItemAdd(ShowQueueItem):
def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime,
scene, paused, blacklist, whitelist, default_wanted_begin, default_wanted_latest, tag,
- scheduled_update=False, new_show=False, show_name=None):
+ scheduled_update=False, new_show=False, show_name=None, upgrade_once=False):
self.indexer = indexer
self.indexer_id = indexer_id
@@ -247,6 +247,7 @@ class QueueItemAdd(ShowQueueItem):
self.default_wanted_begin = default_wanted_begin
self.default_wanted_latest = default_wanted_latest
self.quality = quality
+ self.upgrade_once = upgrade_once
self.flatten_folders = flatten_folders
self.lang = lang
self.subtitles = subtitles
@@ -341,6 +342,7 @@ class QueueItemAdd(ShowQueueItem):
self.show.location = self.showDir
self.show.subtitles = self.subtitles if None is not self.subtitles else sickbeard.SUBTITLES_DEFAULT
self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT
+ self.show.archive_firstmatch = self.upgrade_once
self.show.flatten_folders = self.flatten_folders if None is not self.flatten_folders else sickbeard.FLATTEN_FOLDERS_DEFAULT
self.show.anime = self.anime if None is not self.anime else sickbeard.ANIME_DEFAULT
self.show.scene = self.scene if None is not self.scene else sickbeard.SCENE_DEFAULT
diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py
index b7c5ffae..82de8610 100644
--- a/sickbeard/webapi.py
+++ b/sickbeard/webapi.py
@@ -27,6 +27,10 @@ import re
import traceback
import sickbeard
import webserve
+import glob
+
+from mimetypes import MimeTypes
+from random import randint
from sickbeard import db, logger, exceptions, history, ui, helpers
from sickbeard import encodingKludge as ek
@@ -35,12 +39,16 @@ from sickbeard import image_cache
from sickbeard import classes
from sickbeard import processTV
from sickbeard import network_timezones, sbdatetime
-from sickbeard.exceptions import ex
+from sickbeard.exceptions import ex, MultipleShowObjectsException
from sickbeard.common import SNATCHED, SNATCHED_ANY, SNATCHED_PROPER, SNATCHED_BEST, DOWNLOADED, SKIPPED, UNAIRED, IGNORED, ARCHIVED, WANTED, UNKNOWN
from sickbeard.helpers import remove_article
+from sickbeard.scene_numbering import set_scene_numbering_helper
from common import Quality, qualityPresetStrings, statusStrings
from sickbeard.indexers.indexer_config import *
-from sickbeard.webserve import MainHandler
+from sickbeard.indexers import indexer_config, indexer_api
+from tornado import gen
+from sickbeard.search_backlog import FORCED_BACKLOG
+from sickbeard.webserve import NewHomeAddShows
try:
import json
@@ -49,6 +57,7 @@ except ImportError:
from lib import subliminal
+
dateFormat = "%Y-%m-%d"
dateTimeFormat = "%Y-%m-%d %H:%M"
timeFormat = '%A %I:%M %p'
@@ -68,10 +77,24 @@ result_type_map = {RESULT_SUCCESS: "success",
}
# basically everything except RESULT_SUCCESS / success is bad
+quality_map = {'sdtv': Quality.SDTV,
+ 'sddvd': Quality.SDDVD,
+ 'hdtv': Quality.HDTV,
+ 'rawhdtv': Quality.RAWHDTV,
+ 'fullhdtv': Quality.FULLHDTV,
+ 'hdwebdl': Quality.HDWEBDL,
+ 'fullhdwebdl': Quality.FULLHDWEBDL,
+ 'hdbluray': Quality.HDBLURAY,
+ 'fullhdbluray': Quality.FULLHDBLURAY,
+ 'uhd4kweb': Quality.UHD4KWEB,
+ 'unknown': Quality.UNKNOWN}
+
+quality_map_inversed = {v: k for k, v in quality_map.iteritems()}
+
class Api(webserve.BaseHandler):
""" api class that returns json results """
- version = 4 # use an int since float-point is unpredictible
+ version = 10 # use an int since float-point is unpredictible
intent = 4
def set_default_headers(self):
@@ -79,7 +102,10 @@ class Api(webserve.BaseHandler):
self.set_header('X-Robots-Tag', 'noindex, nofollow, noarchive, nocache, noodp, noydir, noimageindex, nosnippet')
if sickbeard.SEND_SECURITY_HEADERS:
self.set_header('X-Frame-Options', 'SAMEORIGIN')
+ self.set_header('X-Application', 'SickGear')
+ self.set_header('X-API-Version', Api.version)
+ @gen.coroutine
def get(self, route, *args, **kwargs):
route = route.strip('/') or 'index'
@@ -287,6 +313,19 @@ class ApiCall(object):
# RequestHandler
self.handler = handler
+ # old sickbeard call
+ self._sickbeard_call = getattr(self, '_sickbeard_call', False)
+
+ @property
+ def sickbeard_call(self):
+ if hasattr(self, '_sickbeard_call'):
+ return self._sickbeard_call
+ return False
+
+ @sickbeard_call.setter
+ def sickbeard_call(self, v):
+ self._sickbeard_call = v
+
def run(self):
# override with real output function in subclass
return {}
@@ -335,13 +374,13 @@ class ApiCall(object):
msg = "The required parameters: '" + "','".join(self._missing) + "' where not set"
return _responds(RESULT_ERROR, msg=msg)
- def check_params(self, args, kwargs, key, default, required, type, allowedValues):
+ def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None):
# TODO: explain this
""" function to check passed params for the shorthand wrapper
and to detect missing/required param
"""
# Fix for applications that send tvdbid instead of indexerid
- if key == "indexerid" and "indexerid" not in kwargs:
+ if self.sickbeard_call and key == "indexerid" and "indexerid" not in kwargs:
key = "tvdbid"
missing = True
@@ -360,7 +399,8 @@ class ApiCall(object):
if required:
try:
self._missing
- self._requiredParams.append(key)
+ self._requiredParams[key] = {"allowedValues": allowedValues,
+ "defaultValue": orgDefault}
except AttributeError:
self._missing = []
self._requiredParams = {}
@@ -378,14 +418,14 @@ class ApiCall(object):
"defaultValue": orgDefault}
if default:
- default = self._check_param_type(default, key, type)
+ default = self._check_param_type(default, key, type, sub_type)
if type == "bool":
type = []
self._check_param_value(default, key, allowedValues)
return default, args
- def _check_param_type(self, value, name, type):
+ def _check_param_type(self, value, name, type, sub_type):
""" checks if value can be converted / parsed to type
will raise an error on failure
or will convert it to type and return new converted value
@@ -412,7 +452,29 @@ class ApiCall(object):
else:
error = True
elif type == "list":
- value = value.split("|")
+ if None is not sub_type:
+ if sub_type in (int, long):
+ if isinstance(value, (int, long)):
+ value = [value]
+ elif isinstance(value, basestring):
+ if '|' in value:
+ li = [int(v) for v in value.split('|')]
+ if any([not isinstance(v, (int, long)) for v in li]):
+ error = True
+ else:
+ value = li
+ else:
+ value = [int(value)]
+ else:
+ error = True
+ else:
+ li = value.split('|')
+ if any([sub_type is not type(v) for v in li]):
+ error = True
+ else:
+ value = li
+ else:
+ value = value.split("|")
elif type == "string":
pass
elif type == "ignore":
@@ -589,17 +651,7 @@ def _mapQuality(showObj):
def _getQualityMap():
- return {Quality.SDTV: 'sdtv',
- Quality.SDDVD: 'sddvd',
- Quality.HDTV: 'hdtv',
- Quality.RAWHDTV: 'rawhdtv',
- Quality.FULLHDTV: 'fullhdtv',
- Quality.HDWEBDL: 'hdwebdl',
- Quality.FULLHDWEBDL: 'fullhdwebdl',
- Quality.HDBLURAY: 'hdbluray',
- Quality.FULLHDBLURAY: 'fullhdbluray',
- Quality.UNKNOWN: 'unknown'}
-
+ return quality_map_inversed
def _getRootDirs():
if sickbeard.ROOT_DIRS == "":
@@ -651,6 +703,83 @@ class IntParseError(Exception):
# -------------------------------------------------------------------------------------#
+class CMD_ListCommands(ApiCall):
+ _help = {"desc": "list help of all commands",
+ }
+
+ def __init__(self, handler, args, kwargs):
+ # required
+ # optional
+ ApiCall.__init__(self, handler, args, kwargs)
+
+ def run(self):
+ """ display help information for all commands """
+ out = ''
+ table_sickgear_commands = ''
+ table_sickbeard_commands = ''
+ for f, v in sorted(_functionMaper.iteritems(), key=lambda x: (re.sub(r'^s[bg]\.', '', x[0], flags=re.I), re.sub(r'^sg\.', '1', x[0], flags=re.I))):
+ if 'listcommands' == f:
+ continue
+ help = getattr(v, '_help', None)
+ is_old_command = isinstance(help, dict) and "SickGearCommand" in help
+ if is_old_command:
+ table_sickbeard_commands += '%s | ' % f
+ else:
+ table_sickgear_commands += '
%s | ' % f
+ color = ("", " style='color: grey !important;'")[is_old_command]
+ out += '
%s%s
' % (color, f, ("", " (Sickbeard compatibility command)")[is_old_command])
+ if isinstance(help, dict):
+ sg_c = ''
+ if "SickGearCommand" in help:
+ sg_c += '%s | ' % help['SickGearCommand']
+ out += "for all features use SickGear API Command: %s
" % help['SickGearCommand']
+ if "desc" in help:
+ if is_old_command:
+ table_sickbeard_commands += '%s | %s' % (help['desc'], sg_c)
+ else:
+ table_sickgear_commands += '%s | ' % help['desc']
+ out += help['desc']
+
+ table = ''
+
+ if "requiredParameters" in help and isinstance(help['requiredParameters'], dict):
+ for p, d in help['requiredParameters'].iteritems():
+ des = ''
+ if isinstance(d, dict) and 'desc' in d:
+ des = d.get('desc')
+ table += "
%s required | %s |
" % (p, des)
+
+ if "optionalParameters" in help and isinstance(help['optionalParameters'], dict):
+ for p, d in help['optionalParameters'].iteritems():
+ des = ''
+ if isinstance(d, dict) and 'desc' in d:
+ des = d.get('desc')
+ table += "%s optional | %s |
" % (p, des)
+ if table:
+ out += "
Parameter | Description |
"
+ out += table
+ out += '
'
+ else:
+ if is_old_command:
+ table_sickbeard_commands += '