Browse Source

Merge branch 'feature/FixWebApi' into develop

pull/1200/head
JackDandy 6 years ago
parent
commit
3adbda07c9
  1. 20
      CHANGES.md
  2. 1
      HACKS.txt
  3. 2
      gui/slick/css/dark.css
  4. 2
      gui/slick/css/light.css
  5. 13
      gui/slick/css/style.css
  6. 20
      gui/slick/interfaces/default/apiBuilder.tmpl
  7. 22
      gui/slick/interfaces/default/config_general.tmpl
  8. 2
      gui/slick/js/apibuilder.js
  9. 96
      gui/slick/js/config.js
  10. 7
      lib/imdbpie/imdbpie.py
  11. 12
      sickbeard/__init__.py
  12. 5
      sickbeard/indexermapper.py
  13. 2
      sickbeard/tv.py
  14. 243
      sickbeard/webapi.py
  15. 57
      sickbeard/webserve.py

20
CHANGES.md

@ -1,5 +1,25 @@
### 0.21.0 (2019-xx-xx xx:xx:xx UTC)
* Add ability to use multiple SG apikeys
* Add UI for multiple apikeys to config/General/Web Interface
* Add jquery-qrcode 0.17.0
* Change add apikey name to ERROR log messages
* Change add logging of errors from api
* Change add remote ip to error message
* Change add print command name for api in debug log
* Change add warning message to log if old Sick-Beard api call is used
* Change add an api call mapping helper for name changed functions (for printed warnings)
* Change ui typo in apiBuilder
* Fix display of fanart in apibuilder
* Add help command to apiBuilder and fix help call
* Fix add shows via api
* Change fix sg.searchqueue output
* Add missing sg.show.delete parameter "full"
* Add missing sg.setdefaults and sg.shutdown methods
* Change increase api version because missing sg.* methods are added
* Change add some extra checks for Sick-Beard call add (existing) show
* Change patch imdbpie to add cachedir folder and set imdbpie cachedir in SG
* Fix force search return values
* Update attr 19.2.0.dev0 (154b4e5) to 19.2.0.dev0 (daf2bc8)
* Update Beautiful Soup 4.7.1 (r497) to 4.8.0 (r526)
* Update bencode to 2.1.0 (e8290df)

1
HACKS.txt

@ -9,6 +9,7 @@ Libs with customisations...
/lib/hachoir_metadata/riff.py
/lib/hachoir_parser/guess.py
/lib/hachoir_parser/misc/torrent.py
/lib/imdbpie
/lib/lockfile/mkdirlockfile.py
/lib/rtorrent
/lib/scandir/scandir.py

2
gui/slick/css/dark.css

@ -651,10 +651,12 @@ config*.tmpl
border-bottom:1px dotted #666
}
.qr-btn .glyphicon-qrcode,
.component-group-desc p{
color:#ddd
}
.dotted-surround,
.test-notification{
border:1px dotted #ccc
}

2
gui/slick/css/light.css

@ -641,10 +641,12 @@ config*.tmpl
border-bottom:1px dotted #666
}
.qr-btn .glyphicon-qrcode,
.component-group-desc p{
color:#666
}
.dotted-surround,
.test-notification{
border:1px dotted #ccc
}

13
gui/slick/css/style.css

@ -3029,6 +3029,7 @@ select .selected:before{
margin:0 !important
}
.dotted-surround,
.test-notification{
padding:5px;
margin-bottom:10px;
@ -3151,6 +3152,18 @@ select .selected:before{
background-position:-104px 0
}
#api-keys > div{display:inline-block}
#api-keys span{float:left}
#api-keys span, #generate-result{line-height:22px}
#api-keys .api-key{width:235px}
#api-keys .app-name{width:135px}
.qr-btn{margin-right:6px}
.qr-btn .glyphicon-qrcode{cursor:pointer;font-size:15px}
.apikey-qr-dlg .qr-title{padding:0 25px 5px 25px;text-align:right}
.apikey-qr-dlg .qr-title em{color:#999;font-weight:bolder}
.apikey-qr-dlg .qr-title span{color:#333}
.apikey-qr-dlg .qr-body{padding:25px 25px 0}
/* =======================================================================
config_postProcessing.tmpl
========================================================================== */

20
gui/slick/interfaces/default/apiBuilder.tmpl

@ -34,6 +34,12 @@ addListGroup("api", "Command");
addOption("Command", "SickGear", "?cmd=sg", 1); //make default
addOption("Command", "SickBeard", "?cmd=sb");
addOption("Command", "List Commands", "?cmd=listcommands");
addList("Command", "Help", "?cmd=help", "sg.functions-list", "","", "action");
#from sickbeard.webapi import _functionMaper
#from six import iterkeys
#for $k in sorted(iterkeys(_functionMaper), key=lambda x: x.replace('sg.', '').replace('sb.', ''))
addOption("sg.functions-list", "$k", "&subject=$k")
#end for
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");
@ -53,7 +59,7 @@ addList("Command", "SickGear.GetIndexers", "?cmd=sg.getindexers", "listindexers"
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");
addOption("Command", "SickGear.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");
@ -63,7 +69,9 @@ 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");
addList("Command", "SickGear.SetDefaults", "?cmd=sg.setdefaults", "sb.setdefaults", "", "", "action");
addOption("Command", "SickBeard.Shutdown", "?cmd=sb.shutdown", "", "", "action");
addOption("Command", "SickGear.Shutdown", "?cmd=sg.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");
@ -101,7 +109,7 @@ addList("Command", "SickGear.Show.AddNew", "?cmd=sg.show.addnew", "sg.show.addne
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", "SickGear.Show.Delete", "?cmd=sg.show.delete", "show-delete", "", "", "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");
@ -483,6 +491,14 @@ addOption("episode-status", "Skipped", "&status=skipped");
addOption("episode-status", "Archived", "&status=archived");
addOption("episode-status", "Ignored", "&status=ignored");
#for $curShow in $sortedShowList:
addList("show-delete", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "delete-options");
#end for
addOption("delete-options", "Optional Param", "", 1)
addList("delete-options", "Keep Files/Folders", "&full=0")
addList("delete-options", "Delete Files/Folders", "&full=1")
addOption("future", "Optional Param", "", 1);
addList("future", "Sort by Date", "&sort=date", "future-type");
addList("future", "Sort by Network", "&sort=network", "future-type");

22
gui/slick/interfaces/default/config_general.tmpl

@ -496,7 +496,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 (and Legacy SickBeard) API</p>
<p>permit the use of the SickGear (and legacy SickBeard) API</p>
</span>
</label>
</div>
@ -505,10 +505,22 @@
<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>
<input id="app-name" type="text" placeholder="enter app name" class="form-control input-sm input150">
<input id="generate-api-key" value="Generate" type="button" class="btn btn-inline">
<span id="generate-result">&nbsp;</span>
<p class="clear-left">apps using SickGear API calls gain full access, legacy SickBeard endpoints are limited to thetvdb.com shows</p>
<div id="api-keys" class="clear-left">
<div class="new-key highlight-text" style="display:none"><span class="qr-btn"><a rel="" name="qr" title="API key QR code"><span class="glyphicon glyphicon-qrcode"></span></a></span><span class="api-key"></span><span class="app-name"></span><input value="Revoke" type="button" class="revoke btn btn-inline"></div>
#set $tip_addkeys = ''
#for $appname, $uid in $api_keys
#if not ($appname and $uid)
#continue
#end if
<div class="grey-text"><span class="qr-btn"><a rel="qr" name="qr" title="API key QR code"><span class="glyphicon glyphicon-qrcode"></span></a></span><span class="api-key">$uid</span><span class="app-name">$appname</span><input value="Revoke" type="button" class="revoke btn btn-inline"></div>
#set $tip_addkeys = ' style="display:none"'
#end for
<div id="tip-addkeys"$tip_addkeys>Keys used by 3rd party programs to access SickGear will list here when generated</div>
</div>
</span>
</label>
</div>

2
gui/slick/js/apibuilder.js

@ -26,7 +26,7 @@ function goListGroup(apikey, L8, 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=sg.getnetworkicon" || L1 == "?cmd=sg.show.getposter" || L1 == "?cmd=sg.show.getbanner" || L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner" || L1 == "?cmd=sg.getindexericon") {
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" || L1 == "?cmd=sg.show.getfanart") {
var imgcache = sbRoot + "/api/" + apikey + "/" + L1 + L2 + GlobalOptions;
var html = imgcache + '<br/><br/><img src="' + sbRoot + '/images/loading16.gif" id="imgcache">';
$('#apiResponse').html(html);

96
gui/slick/js/config.js

File diff suppressed because one or more lines are too long

7
lib/imdbpie/imdbpie.py

@ -53,12 +53,15 @@ _SIMPLE_GET_ENDPOINTS = {
class Imdb(Auth):
def __init__(self, locale=None, exclude_episodes=False, session=None):
def __init__(self, locale=None, exclude_episodes=False, session=None, cachedir=None):
self.locale = locale or 'en_US'
self.region = self.locale.split('_')[-1].upper()
self.exclude_episodes = exclude_episodes
self.session = session or requests.Session()
self._cachedir = tempfile.gettempdir()
if not cachedir:
self._cachedir = tempfile.gettempdir()
else:
self._cachedir = cachedir
def __getattr__(self, name):
if name in _SIMPLE_GET_ENDPOINTS:

12
sickbeard/__init__.py

@ -146,7 +146,7 @@ CPU_PRESET = 'DISABLED'
ANON_REDIRECT = None
USE_API = False
API_KEY = None
API_KEYS = []
ENABLE_HTTPS = False
HTTPS_CERT = None
@ -619,7 +619,7 @@ def init_stage_1(console_logging):
global THEME_NAME, DEFAULT_HOME, FANART_LIMIT, SHOWLIST_TAGVIEW, SHOW_TAGS, \
HOME_SEARCH_FOCUS, USE_IMDB_INFO, IMDB_ACCOUNTS, DISPLAY_FREESPACE, SORT_ARTICLE, FUZZY_DATING, TRIM_ZERO, \
DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, TIMEZONE_DISPLAY, \
WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEY, WEB_PORT, WEB_LOG, \
WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEYS, WEB_PORT, WEB_LOG, \
ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY, \
SEND_SECURITY_HEADERS, ALLOWED_HOSTS
# Gen Config/Advanced
@ -814,7 +814,11 @@ def init_stage_1(console_logging):
TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0))
USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0))
API_KEY = check_setting_str(CFG, 'General', 'api_key', '')
API_KEYS = [k.split(':::') for k in check_setting_str(CFG, 'General', 'api_keys', '').split('|||') if k]
if not API_KEYS:
tmp_api_key = check_setting_str(CFG, 'General', 'api_key', None)
if None is not tmp_api_key:
API_KEYS = [['app name (old key)', tmp_api_key]]
DEBUG = bool(check_setting_int(CFG, 'General', 'debug', 0))
@ -1668,7 +1672,7 @@ def save_config():
new_config['General']['cpu_preset'] = CPU_PRESET
new_config['General']['anon_redirect'] = ANON_REDIRECT
new_config['General']['use_api'] = int(USE_API)
new_config['General']['api_key'] = API_KEY
new_config['General']['api_keys'] = '|||'.join([':::'.join(a) for a in API_KEYS])
new_config['General']['debug'] = int(DEBUG)
new_config['General']['enable_https'] = int(ENABLE_HTTPS)
new_config['General']['https_cert'] = HTTPS_CERT

5
sickbeard/indexermapper.py

@ -17,6 +17,7 @@
import datetime
import re
import traceback
import os
import requests
import sickbeard
@ -26,7 +27,7 @@ from lib.dateutil.parser import parse
from lib.unidecode import unidecode
from libtrakt import TraktAPI
from libtrakt.exceptions import TraktAuthException, TraktException
from sickbeard import db, logger
from sickbeard import db, logger, encodingKludge as ek
from sickbeard.helpers import tryInt, getURL
from sickbeard.indexers.indexer_config import (INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TVMAZE,
INDEXER_IMDB, INDEXER_TRAKT, INDEXER_TMDB)
@ -198,7 +199,7 @@ def get_trakt_ids(url_trakt):
def get_imdbid_by_name(name, startyear):
ids = {}
try:
res = Imdb(exclude_episodes=True).search_for_title(title=name)
res = Imdb(exclude_episodes=True, cachedir=ek.ek(os.path.join, sickbeard.CACHE_DIR, 'imdb-pie')).search_for_title(title=name)
for r in res:
if isinstance(r.get('type'), basestring) and 'tv series' == r.get('type').lower() \
and str(startyear) == str(r.get('year')):

2
sickbeard/tv.py

@ -1116,7 +1116,7 @@ class TVShow(object):
self._imdbid = redirect_check
imdb_id = redirect_check
imdb_info['imdb_id'] = self.imdbid
i = imdbpie.Imdb(exclude_episodes=True)
i = imdbpie.Imdb(exclude_episodes=True, cachedir=ek.ek(os.path.join, sickbeard.CACHE_DIR, 'imdb-pie'))
if not re.search(r'tt\d{7}', imdb_id, flags=re.I):
logger.log('Not a valid imdbid: %s for show: %s' % (imdb_id, self.name), logger.WARNING)
return

243
sickbeard/webapi.py

@ -46,9 +46,11 @@ from sickbeard.scene_numbering import set_scene_numbering_helper
from common import Quality, qualityPresetStrings, statusStrings
from sickbeard.indexers.indexer_config import *
from sickbeard.indexers import indexer_config, indexer_api
from sickbeard.tv import TVShow, TVEpisode
from tornado import gen
from sickbeard.search_backlog import FORCED_BACKLOG
from sickbeard.webserve import NewHomeAddShows
from six import iteritems, integer_types
try:
import json
@ -92,6 +94,13 @@ quality_map = {'sdtv': Quality.SDTV,
quality_map_inversed = {v: k for k, v in quality_map.iteritems()}
def api_log(obj, msg, level=logger.MESSAGE):
apikey_name = getattr(obj, 'apikey_name', '')
if apikey_name:
apikey_name = ' (%s)' % apikey_name
logger.log('%s%s' % ('API%s:: ' % apikey_name, msg), level)
class ApiServerLoading(webserve.BaseHandler):
@gen.coroutine
def get(self, route, *args, **kwargs):
@ -104,12 +113,16 @@ class PythonObjectEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
elif isinstance(obj, TVEpisode):
return {'season': obj.season, 'episode': obj.episode}
elif isinstance(obj, TVShow):
return {'name': obj.name, 'indexer': obj.indexer, 'indexer_id': obj.indexerid}
return json.JSONEncoder.default(self, obj)
class Api(webserve.BaseHandler):
""" api class that returns json results """
version = 10 # use an int since float-point is unpredictible
version = 11 # use an int since float-point is unpredictible
intent = 4
def check_xsrf_cookie(self):
@ -147,8 +160,8 @@ class Api(webserve.BaseHandler):
args = args[1:]
self.apiKey = sickbeard.API_KEY
access, accessMsg, args, kwargs = self._grand_access(self.apiKey, route, args, kwargs)
self.apiKeys = sickbeard.API_KEYS
access, accessMsg, args, kwargs = self._grand_access(self.apiKeys, route, args, kwargs)
# set the output callback
# default json
@ -158,9 +171,9 @@ class Api(webserve.BaseHandler):
# do we have acces ?
if access:
logger.log(accessMsg, logger.DEBUG)
api_log(self, accessMsg, logger.DEBUG)
else:
logger.log(accessMsg, logger.WARNING)
api_log(self, accessMsg, logger.WARNING)
return outputCallbackDict['default'](_responds(RESULT_DENIED, msg=accessMsg))
# set the original call_dispatcher as the local _call_dispatcher
@ -181,7 +194,7 @@ class Api(webserve.BaseHandler):
try:
outDict = _call_dispatcher(self, args, kwargs)
except Exception as e: # real internal error oohhh nooo :(
logger.log(u"API :: " + ex(e), logger.ERROR)
api_log(self, ex(e), logger.ERROR)
errorData = {"error_msg": ex(e),
"args": args,
"kwargs": kwargs}
@ -203,28 +216,30 @@ class Api(webserve.BaseHandler):
out = '%s(%s);' % (callback, out) # wrap with JSONP call if requested
except Exception as e: # if we fail to generate the output fake an error
logger.log(u'API :: ' + traceback.format_exc(), logger.ERROR)
api_log(self, traceback.format_exc(), logger.ERROR)
out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex(
e) + '"}'
return out
def _grand_access(self, realKey, apiKey, args, kwargs):
def _grand_access(self, realKeys, apiKey, args, kwargs):
""" validate api key and log result """
remoteIp = self.request.remote_ip
self.apikey_name = ''
if not sickbeard.USE_API:
msg = u'API :: ' + remoteIp + ' - SB API Disabled. ACCESS DENIED'
return False, msg, args, kwargs
elif apiKey == realKey:
msg = u'API :: ' + remoteIp + ' - gave correct API KEY. ACCESS GRANTED'
return True, msg, args, kwargs
elif not apiKey:
msg = u'API :: ' + remoteIp + ' - gave NO API KEY. ACCESS DENIED'
msg = u'%s - SB API Disabled. ACCESS DENIED' % remoteIp
return False, msg, args, kwargs
else:
msg = u'API :: ' + remoteIp + ' - gave WRONG API KEY ' + apiKey + '. ACCESS DENIED'
if not apiKey:
msg = u'%s - gave NO API KEY. ACCESS DENIED' % remoteIp
return False, msg, args, kwargs
for realKey in realKeys:
if apiKey == realKey[1]:
self.apikey_name = realKey[0]
msg = u'%s - gave correct API KEY: %s. ACCESS GRANTED' % (remoteIp, realKey[0])
return True, msg, args, kwargs
msg = u'%s - gave WRONG API KEY %s. ACCESS DENIED' % (remoteIp, apiKey)
return False, msg, args, kwargs
def call_dispatcher(handler, args, kwargs):
@ -233,10 +248,6 @@ def call_dispatcher(handler, args, kwargs):
or calls the TVDBShorthandWrapper when the first args element is a number
or returns an error that there is no such cmd
"""
logger.log(u"API :: all args: '" + str(args) + "'", logger.DEBUG)
logger.log(u"API :: all kwargs: '" + str(kwargs) + "'", logger.DEBUG)
# logger.log(u"API :: dateFormat: '" + str(dateFormat) + "'", logger.DEBUG)
cmds = None
if args:
cmds = args[0]
@ -246,6 +257,12 @@ def call_dispatcher(handler, args, kwargs):
cmds = kwargs["cmd"]
del kwargs["cmd"]
api_log(handler, u"cmd: '" + str(cmds) + "'", logger.DEBUG)
api_log(handler, u"all args: '" + str(args) + "'", logger.DEBUG)
api_log(handler, u"all kwargs: '" + str(kwargs) + "'", logger.DEBUG)
# logger.log(u"dateFormat: '" + str(dateFormat) + "'", logger.DEBUG)
outDict = {}
if cmds != None:
cmds = cmds.split("|")
@ -256,7 +273,7 @@ def call_dispatcher(handler, args, kwargs):
if len(cmd.split("_")) > 1: # was a index used for this cmd ?
cmd, cmdIndex = cmd.split("_") # this gives us the clear cmd and the index
logger.log(u"API :: " + cmd + ": curKwargs " + str(curKwargs), logger.DEBUG)
api_log(handler, cmd + ": curKwargs " + str(curKwargs), logger.DEBUG)
if not (multiCmds and cmd in ('show.getposter', 'show.getbanner')): # skip these cmd while chaining
try:
if cmd in _functionMaper:
@ -346,6 +363,12 @@ class ApiCall(object):
# old sickbeard call
self._sickbeard_call = getattr(self, '_sickbeard_call', False)
if 'help' not in kwargs and self._sickbeard_call:
call_name = _functionMaper_reversed.get(self.__class__, '')
if 'sb' != call_name:
self.log('SickBeard API call "%s" should be replaced with SickGear API "%s" calls to get much '
'improved detail and functionality, contact your App developer and ask them to update '
'their code.' % (call_name, self._get_old_command()), logger.WARNING)
@property
def sickbeard_call(self):
@ -361,6 +384,24 @@ class ApiCall(object):
# override with real output function in subclass
return {}
def log(self, msg, level=logger.MESSAGE):
api_log(self.handler, msg, level)
def _get_old_command(self, command_class=None):
c_class = command_class or self
new_call_name = None
help = getattr(c_class, '_help', None)
if getattr(c_class, '_sickbeard_call', False) or "SickGearCommand" in help:
call_name = _functionMaper_reversed.get(c_class.__class__, '')
new_call_name = 'sg.%s' % call_name.replace('sb.', '') if 'sb' != call_name else 'sg'
if new_call_name not in _functionMaper:
if isinstance(help, dict) and "SickGearCommand" in help \
and help['SickGearCommand'] in _functionMaper:
new_call_name = help['SickGearCommand']
else:
new_call_name = 'sg.*'
return new_call_name
def return_help(self):
try:
if self._requiredParams:
@ -403,6 +444,13 @@ class ApiCall(object):
msg = "The required parameter: '" + self._missing[0] + "' was not set"
else:
msg = "The required parameters: '" + "','".join(self._missing) + "' where not set"
try:
remote_ip = self.handler.request.remote_ip
except (BaseException, Exception):
remote_ip = '"unknown ip"'
self.log("API call from host %s triggers :: %s: %s" %
(remote_ip, _functionMaper_reversed.get(self.__class__, ''), msg),
logger.ERROR)
return _responds(RESULT_ERROR, msg=msg)
def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None):
@ -516,7 +564,7 @@ class ApiCall(object):
elif type == "ignore":
pass
else:
logger.log(u"API :: Invalid param type set " + str(type) + " can not check or convert ignoring it",
self.log(u"Invalid param type set " + str(type) + " can not check or convert ignoring it",
logger.ERROR)
if error:
@ -765,13 +813,14 @@ class CMD_ListCommands(ApiCall):
color = ("", " style='color: grey !important;'")[is_old_command]
out += '<hr><h1 class="command"%s>%s%s</h1>' % (color, f, ("", " <span style='font-size: 50%;color: black;'>(Sickbeard compatibility command)</span>")[is_old_command])
if isinstance(help, dict):
sg_c = ''
if "SickGearCommand" in help:
sg_c += '<td>%s</td>' % help['SickGearCommand']
out += "<p style='color: darkgreen !important;'>for all features use SickGear API Command: <b>%s</b></p>" % help['SickGearCommand']
sg_cmd_new = self._get_old_command(command_class=v)
sg_cmd = ''
if sg_cmd_new:
sg_cmd = '<td>%s</td>' % sg_cmd_new
out += "<p style='color: darkgreen !important;'>for all features use SickGear API Command: <b>%s</b></p>" % sg_cmd_new
if "desc" in help:
if is_old_command:
table_sickbeard_commands += '<td>%s</td>%s' % (help['desc'], sg_c)
table_sickbeard_commands += '<td>%s</td>%s' % (help['desc'], sg_cmd)
else:
table_sickgear_commands += '<td>%s</td>' % help['desc']
out += help['desc']
@ -832,7 +881,7 @@ class CMD_Help(ApiCall):
def run(self):
""" display help information for a given subject/command """
if self.subject in _functionMaper:
out = _responds(RESULT_SUCCESS, _functionMaper.get(self.subject)((), {"help": 1}).run())
out = _responds(RESULT_SUCCESS, _functionMaper.get(self.subject)(None, (), {"help": 1}).run())
else:
out = _responds(RESULT_FAILURE, msg="No such cmd")
return out
@ -1302,7 +1351,7 @@ class CMD_SickGearEpisodeSetStatus(ApiCall):
cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment)
sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable
logger.log(u"API :: Starting backlog for " + showObj.name + " season " + str(
self.log(u"Starting backlog for " + showObj.name + " season " + str(
season) + " because some episodes were set to WANTED")
extra_msg = " Backlog started"
@ -2051,11 +2100,15 @@ class CMD_SickGearForceSearch(ApiCall):
def run(self):
""" force the given search type search """
result = None
if 'recent' == self.searchtype:
if 'recent' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_recentsearch_in_progress() \
and not sickbeard.recentSearchScheduler.action.amActive:
result = sickbeard.recentSearchScheduler.forceRun()
elif 'backlog' == self.searchtype:
result = sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG)
elif 'proper' == self.searchtype:
elif 'backlog' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_backlog_in_progress() \
and not sickbeard.backlogSearchScheduler.action.amActive:
sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG)
result = True
elif 'proper' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_propersearch_in_progress() \
and not sickbeard.properFinderScheduler.action.amActive:
result = sickbeard.properFinderScheduler.forceRun()
if result:
return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype)
@ -2106,7 +2159,7 @@ class CMD_SickGearGetDefaults(ApiCall):
data = {"status": statusStrings[sickbeard.STATUS_DEFAULT].lower(),
"flatten_folders": int(sickbeard.FLATTEN_FOLDERS_DEFAULT), "initial": anyQualities,
"archive": bestQualities, "future_show_paused": int(sickgear.EPISODE_VIEW_DISPLAY_PAUSED)}
"archive": bestQualities, "future_show_paused": int(sickbeard.EPISODE_VIEW_DISPLAY_PAUSED)}
return _responds(RESULT_SUCCESS, data)
@ -2470,12 +2523,12 @@ class CMD_SickGearSearchIndexers(ApiCall):
try:
myShow = t[int(self.indexerid), False]
except (sickbeard.indexer_shownotfound, sickbeard.indexer_error):
logger.log(u"API :: Unable to find show with id " + str(self.indexerid), logger.WARNING)
self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING)
return _responds(RESULT_SUCCESS, {"results": [], "langid": lang_id})
if not myShow.data['seriesname']:
logger.log(
u"API :: Found show with indexerid " + str(self.indexerid) + ", however it contained no show name",
self.log(
u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name",
logger.DEBUG)
return _responds(RESULT_FAILURE, msg="Show contains no name, invalid result")
@ -2514,7 +2567,7 @@ class CMD_SickBeardSearchIndexers(CMD_SickGearSearchIndexers):
CMD_SickGearSearchIndexers.__init__(self, handler, args, kwargs)
class CMD_SickBeardSetDefaults(ApiCall):
class CMD_SickGearSetDefaults(ApiCall):
_help = {"desc": "set sickgear user defaults",
"optionalParameters": {"initial": {"desc": "initial quality for the show"},
"archive": {"desc": "archive quality for the show"},
@ -2574,6 +2627,22 @@ class CMD_SickBeardSetDefaults(ApiCall):
return _responds(RESULT_SUCCESS, msg="Saved defaults")
class CMD_SickBeardSetDefaults(CMD_SickGearSetDefaults):
_help = {"desc": "set sickgear user defaults",
"optionalParameters": {"initial": {"desc": "initial quality for the show"},
"archive": {"desc": "archive quality for the show"},
"flatten_folders": {"desc": "flatten subfolders within the show directory"},
"status": {"desc": "status of missing episodes"}
},
"SickGearCommand": "sg.setdefaults",
}
def __init__(self, handler, args, kwargs):
# super, missing, help
self.sickbeard_call = True
CMD_SickGearSetDefaults.__init__(self, handler, args, kwargs)
class CMD_SickGearSetSceneNumber(ApiCall):
_help = {"desc": "set Scene Numbers",
"requiredParameters": {"indexerid": {"desc": "unique id of a show"},
@ -2647,7 +2716,7 @@ class CMD_SickGearActivateSceneNumber(ApiCall):
msg="Scene Numbering %sactivated" % ('de', '')[self.activate])
class CMD_SickBeardShutdown(ApiCall):
class CMD_SickGearShutdown(ApiCall):
_help = {"desc": "shutdown sickgear"}
def __init__(self, handler, args, kwargs):
@ -2662,6 +2731,16 @@ class CMD_SickBeardShutdown(ApiCall):
return _responds(RESULT_SUCCESS, msg="SickGear is shutting down...")
class CMD_SickBeardShutdown(CMD_SickGearShutdown):
_help = {"desc": "shutdown sickgear",
"SickGearCommand": "sg.shutdown",
}
def __init__(self, handler, args, kwargs):
self.sickbeard_call = True
CMD_SickGearShutdown.__init__(self, handler, args, kwargs)
class CMD_SickGearListIgnoreWords(ApiCall):
_help = {"desc": "list ignore words",
"optionalParameters": {"indexerid": {"desc": "unique id of a show"},
@ -2959,8 +3038,11 @@ class CMD_SickGearShow(ApiCall):
return _responds(RESULT_FAILURE, msg="Show not found")
showDict = {}
showDict["season_list"] = CMD_ShowSeasonList(self.handler, (), {"indexerid": self.indexerid}).run()["data"]
showDict["cache"] = CMD_ShowCache(self.handler, (), {"indexerid": self.indexerid}).run()["data"]
showDict["season_list"] = CMD_SickGearShowSeasonList(self.handler, (),
{"indexer": self.indexer, "indexerid": self.indexerid}
).run()["data"]
showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": self.indexer,
"indexerid": self.indexerid}).run()["data"]
genreList = []
if showObj.genre:
@ -3083,15 +3165,28 @@ class CMD_SickGearShowAddExisting(ApiCall):
if not ek.ek(os.path.isdir, self.location):
return _responds(RESULT_FAILURE, msg='Not a valid location')
lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy()
lINDEXER_API_PARMS['language'] = 'en'
lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI
lINDEXER_API_PARMS['actors'] = False
t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS)
try:
myShow = t[int(self.indexerid), False]
except (sickbeard.indexer_shownotfound, sickbeard.indexer_error):
self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING)
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
indexerName = None
indexerResult = CMD_SickBeardSearchIndexers(self.handler, [],
{"indexerid": self.indexerid, "indexer": self.indexer}).run()
if not myShow.data['seriesname']:
self.log(
u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name",
logger.DEBUG)
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
if indexerResult['result'] == result_type_map[RESULT_SUCCESS]:
if not indexerResult['data']['results']:
return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again")
if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]:
indexerName = indexerResult['data']['results'][0]['name']
else:
indexerName = myShow.data['seriesname']
if not indexerName:
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
@ -3134,7 +3229,8 @@ class CMD_ShowAddExisting(CMD_SickGearShowAddExisting):
def __init__(self, handler, args, kwargs):
kwargs['indexer'] = INDEXER_TVDB
# required
kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", [])
if 'tvdbid' in kwargs and 'indexerid' not in kwargs:
kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", [])
# super, missing, help
self.sickbeard_call = True
CMD_SickGearShowAddExisting.__init__(self, handler, args, kwargs)
@ -3231,15 +3327,28 @@ class CMD_SickGearShowAddNew(ApiCall):
return _responds(RESULT_FAILURE, msg="Status prohibited")
newStatus = self.status
lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy()
lINDEXER_API_PARMS['language'] = 'en'
lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI
lINDEXER_API_PARMS['actors'] = False
t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS)
try:
myShow = t[int(self.indexerid), False]
except (sickbeard.indexer_shownotfound, sickbeard.indexer_error):
self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING)
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
indexerName = None
indexerResult = CMD_SickBeardSearchIndexers(self.handler, [],
{"indexerid": self.indexerid, "indexer": self.indexer}).run()
if not myShow.data['seriesname']:
self.log(
u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name",
logger.DEBUG)
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
if indexerResult['result'] == result_type_map[RESULT_SUCCESS]:
if not indexerResult['data']['results']:
return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again")
if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]:
indexerName = indexerResult['data']['results'][0]['name']
else:
indexerName = myShow.data['seriesname']
if not indexerName:
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")
@ -3249,11 +3358,11 @@ class CMD_SickGearShowAddNew(ApiCall):
# don't create show dir if config says not to
if sickbeard.ADD_SHOWS_WO_DIR:
logger.log(u"Skipping initial creation of " + showPath + " due to config.ini setting")
self.log(u"Skipping initial creation of " + showPath + " due to config.ini setting")
else:
dir_exists = helpers.makeDir(showPath)
if not dir_exists:
logger.log(u"API :: Unable to create the folder " + showPath + ", can't add the show", logger.ERROR)
self.log(u"Unable to create the folder " + showPath + ", can't add the show", logger.ERROR)
return _responds(RESULT_FAILURE, {"path": showPath},
"Unable to create the folder " + showPath + ", can't add the show")
else:
@ -3292,7 +3401,8 @@ class CMD_ShowAddNew(CMD_SickGearShowAddNew):
def __init__(self, handler, args, kwargs):
kwargs['indexer'] = INDEXER_TVDB
kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", [])
if 'tvdbid' in kwargs and 'indexerid' not in kwargs:
kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", [])
# required
# optional
# super, missing, help
@ -3359,6 +3469,7 @@ class CMD_SickGearShowDelete(ApiCall):
"requiredParameters": {"indexer": {"desc": "indexer of a show"},
"indexerid": {"desc": "unique id of a show"},
},
"optionalParameters": {"full": {"desc": "delete files/folder of show"}}
}
def __init__(self, handler, args, kwargs):
@ -3367,6 +3478,7 @@ class CMD_SickGearShowDelete(ApiCall):
[i for i in indexer_api.indexerApi().indexers])
self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
# optional
self.full_delete, args = self.check_params(args, kwargs, "full", False, False, "bool", [])
# super, missing, help
ApiCall.__init__(self, handler, args, kwargs)
@ -3380,7 +3492,7 @@ class CMD_SickGearShowDelete(ApiCall):
showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable
return _responds(RESULT_FAILURE, msg="Show can not be deleted while being added or updated")
showObj.deleteShow()
showObj.deleteShow(full=self.full_delete)
return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has been deleted")
@ -4139,7 +4251,7 @@ class CMD_SickGearShowUpdate(ApiCall):
sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable
return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has queued to be updated")
except exceptions.CantUpdateException as e:
logger.log(u"API:: Unable to update " + str(showObj.name) + ". " + str(ex(e)), logger.ERROR)
self.log(u"Unable to update " + str(showObj.name) + ". " + str(ex(e)), logger.ERROR)
return _responds(RESULT_FAILURE, msg="Unable to update " + str(showObj.name))
@ -4230,7 +4342,8 @@ class CMD_SickGearShows(ApiCall):
else:
showDict['next_ep_airdate'] = ''
showDict["cache"] = CMD_ShowCache(self.handler, (), {"indexerid": curShow.indexerid}).run()["data"]
showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": curShow.indexer,
"indexerid": curShow.indexerid}).run()["data"]
if not showDict["network"]:
showDict["network"] = ""
if self.sort == "name":
@ -4459,12 +4572,14 @@ _functionMaper = {"help": CMD_Help,
"sb.searchtvdb": CMD_SickBeardSearchIndexers,
"sg.searchtv": CMD_SickGearSearchIndexers,
"sb.setdefaults": CMD_SickBeardSetDefaults,
"sg.setdefaults": CMD_SickGearSetDefaults,
"sg.setscenenumber": CMD_SickGearSetSceneNumber,
"sg.activatescenenumbering": CMD_SickGearActivateSceneNumber,
"sg.getindexers": CMD_SickGearGetIndexers,
"sg.getindexericon": CMD_SickGearGetIndexerIcon,
"sg.getnetworkicon": CMD_SickGearGetNetworkIcon,
"sb.shutdown": CMD_SickBeardShutdown,
"sg.shutdown": CMD_SickGearShutdown,
"sg.listignorewords": CMD_SickGearListIgnoreWords,
"sg.setignorewords": CMD_SickGearSetIgnoreWords,
"sg.listrequiredwords": CMD_SickGearListRequireWords,
@ -4512,3 +4627,5 @@ _functionMaper = {"help": CMD_Help,
"sg.shows.forceupdate": CMD_SickGearShowsForceUpdate,
"sg.shows.queue": CMD_SickGearShowsQueue,
}
_functionMaper_reversed = {v: k for k, v in iteritems(_functionMaper)}

57
sickbeard/webserve.py

@ -5854,6 +5854,8 @@ class ConfigGeneral(Config):
t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers
if sickbeard.indexerApi(i).config['active']])
t.request_host = escape.xhtml_escape(self.request.host_name)
api_keys = '|||'.join([':::'.join(a) for a in sickbeard.API_KEYS])
t.api_keys = api_keys and sickbeard.API_KEYS or []
return t.respond()
def saveRootDirs(self, rootDirString=None):
@ -5891,7 +5893,8 @@ class ConfigGeneral(Config):
sickbeard.save_config()
def generateKey(self, *args, **kwargs):
@staticmethod
def generateKey(*args, **kwargs):
""" Return a new randomized API_KEY
"""
@ -5911,20 +5914,60 @@ class ConfigGeneral(Config):
m.update(r)
# Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b
logger.log(u'New API generated')
app_name = kwargs.get('app_name')
app_name = '' if not app_name else ' for [%s]' % app_name
logger.log(u'New apikey generated%s' % app_name)
return m.hexdigest()
def create_apikey(self, app_name):
result = dict()
if not app_name:
result['result'] = 'Failed: no name given'
elif app_name in [k[0] for k in sickbeard.API_KEYS if k[0]]:
result['result'] = 'Failed: name is not unique'
else:
api_key = self.generateKey(app_name=app_name)
if api_key in [k[1] for k in sickbeard.API_KEYS if k[0]]:
result['result'] = 'Failed: apikey already exists, try again'
else:
sickbeard.API_KEYS.append([app_name, api_key])
logger.log('Created apikey for [%s]' % app_name, logger.DEBUG)
result.update(dict(result='Success: apikey added', added=api_key))
sickbeard.USE_API = 1
sickbeard.save_config()
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
return json.dumps(result)
@staticmethod
def revoke_apikey(app_name, api_key):
result = dict()
if not app_name:
result['result'] = 'Failed: no name given'
elif not api_key or 32 != len(re.sub('(?i)[^0-9a-f]', '', api_key)):
result['result'] = 'Failed: key not valid'
elif api_key not in [k[1] for k in sickbeard.API_KEYS if k[0]]:
result['result'] = 'Failed: key doesn\'t exist'
else:
sickbeard.API_KEYS = [ak for ak in sickbeard.API_KEYS if ak[0] and api_key != ak[1]]
logger.log('Revoked [%s] apikey [%s]' % (app_name, api_key), logger.DEBUG)
result.update(dict(result='Success: apikey removed', removed=True))
sickbeard.save_config()
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
return json.dumps(result)
def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, web_ipv64=None,
update_shows_on_start=None, show_update_hour=None,
trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None,
use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None,
use_api=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None,
web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None,
handle_reverse_proxy=None, send_security_headers=None, allowed_hosts=None,
home_search_focus=None, display_freespace=None, sort_article=None, auto_update=None, notify_on_update=None,
proxy_setting=None, proxy_indexers=None, anon_redirect=None, git_path=None, git_remote=None, calendar_unprotected=None,
fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None,
indexer_timeout=None, rootDir=None, show_dirs_with_dots=None, theme_name=None, default_home=None,
use_imdb_info=None, fanart_limit=None, show_tags=None, showlist_tagview=None):
use_imdb_info=None, fanart_limit=None, show_tags=None, showlist_tagview=None, **kwargs):
results = []
@ -6000,7 +6043,6 @@ class ConfigGeneral(Config):
sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected)
sickbeard.USE_API = config.checkbox_to_value(use_api)
sickbeard.API_KEY = api_key
sickbeard.WEB_PORT = config.to_int(web_port)
# sickbeard.WEB_LOG is set in config.change_log_dir()
@ -7313,8 +7355,9 @@ class ApiBuilder(MainHandler):
t.indexers = sickbeard.indexerApi().all_indexers
t.searchindexers = sickbeard.indexerApi().search_indexers
if len(sickbeard.API_KEY) == 32:
t.apikey = sickbeard.API_KEY
if len(sickbeard.API_KEYS):
# use first APIKEY for apibuilder tests
t.apikey = sickbeard.API_KEYS[0][1]
else:
t.apikey = 'api key not generated'

Loading…
Cancel
Save