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) ### 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 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 Beautiful Soup 4.7.1 (r497) to 4.8.0 (r526)
* Update bencode to 2.1.0 (e8290df) * Update bencode to 2.1.0 (e8290df)

1
HACKS.txt

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

2
gui/slick/css/dark.css

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

2
gui/slick/css/light.css

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

13
gui/slick/css/style.css

@ -3029,6 +3029,7 @@ select .selected:before{
margin:0 !important margin:0 !important
} }
.dotted-surround,
.test-notification{ .test-notification{
padding:5px; padding:5px;
margin-bottom:10px; margin-bottom:10px;
@ -3151,6 +3152,18 @@ select .selected:before{
background-position:-104px 0 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 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", "SickGear", "?cmd=sg", 1); //make default
addOption("Command", "SickBeard", "?cmd=sb"); addOption("Command", "SickBeard", "?cmd=sb");
addOption("Command", "List Commands", "?cmd=listcommands"); 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", "SickBeard.AddRootDir", "?cmd=sb.addrootdir", "sb.addrootdir", "", "", "action");
addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sb.addrootdir", "", "", "action"); addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sb.addrootdir", "", "", "action");
addOption("Command", "SickBeard.CheckScheduler", "?cmd=sb.checkscheduler", "", "", "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.GetIndexerIcon", "?cmd=sg.getindexericon", "getindexericon", "", "action");
addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon", "", "action"); addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon", "", "action");
addOption("Command", "SickBeard.GetRootDirs", "?cmd=sb.getrootdirs", "", "", "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", "SickBeard.PauseBacklog", "?cmd=sb.pausebacklog", "sb.pausebacklog", "", "", "action");
addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog", "", "", "action"); addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog", "", "", "action");
addOption("Command", "SickBeard.Ping", "?cmd=sb.ping", "", "", "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", "SickBeard.SearchTVDB", "?cmd=sb.searchtvdb", "sb.searchtvdb", "", "", "action");
addList("Command", "SickGear.SearchTV", "?cmd=sg.searchtv", "sg.searchtv", "", "", "action"); addList("Command", "SickGear.SearchTV", "?cmd=sg.searchtv", "sg.searchtv", "", "", "action");
addList("Command", "SickBeard.SetDefaults", "?cmd=sb.setdefaults", "sb.setdefaults", "", "", "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", "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.ListIgnoreWords", "?cmd=sg.listignorewords", "listignorewords", "", "action");
addList("Command", "SickGear.SetIgnoreWords", "?cmd=sg.setignorewords", "setwords", "", "action"); addList("Command", "SickGear.SetIgnoreWords", "?cmd=sg.setignorewords", "setwords", "", "action");
addList("Command", "SickGear.ListRequiredWords", "?cmd=sg.listrequiredwords", "listrequiredwords", "", "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", "Show.Cache", "?cmd=show.cache", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.Cache", "?cmd=sg.show.cache", "sg.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", "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", "Show.GetBanner", "?cmd=show.getbanner", "indexerid", "", "", "action");
addList("Command", "SickGear.Show.GetBanner", "?cmd=sg.show.getbanner", "sg.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.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", "Archived", "&status=archived");
addOption("episode-status", "Ignored", "&status=ignored"); 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); addOption("future", "Optional Param", "", 1);
addList("future", "Sort by Date", "&sort=date", "future-type"); addList("future", "Sort by Date", "&sort=date", "future-type");
addList("future", "Sort by Network", "&sort=network", "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-title">API enable</span>
<span class="component-desc"> <span class="component-desc">
<input type="checkbox" name="use_api" class="enabler" id="use_api"#echo ('', $checked)[$sg_var('USE_API')]#> <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> </span>
</label> </label>
</div> </div>
@ -505,10 +505,22 @@
<label for="api_key"> <label for="api_key">
<span class="component-title">API key</span> <span class="component-title">API key</span>
<span class="component-desc"> <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 id="app-name" type="text" placeholder="enter app name" class="form-control input-sm input150">
<input type="text" name="api_key" id="api_key" value="$sg_str('API_KEY')" class="form-control input-sm input300" readonly="readonly"> <input id="generate-api-key" value="Generate" type="button" class="btn btn-inline">
<input class="btn btn-inline" type="button" id="generate_new_apikey" value="Generate"> <span id="generate-result">&nbsp;</span>
<div class="clear-left"><p>used to give 3rd party programs limited access to SickGear</p></div> <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> </span>
</label> </label>
</div> </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 // 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 imgcache = sbRoot + "/api/" + apikey + "/" + L1 + L2 + GlobalOptions;
var html = imgcache + '<br/><br/><img src="' + sbRoot + '/images/loading16.gif" id="imgcache">'; var html = imgcache + '<br/><br/><img src="' + sbRoot + '/images/loading16.gif" id="imgcache">';
$('#apiResponse').html(html); $('#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): 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.locale = locale or 'en_US'
self.region = self.locale.split('_')[-1].upper() self.region = self.locale.split('_')[-1].upper()
self.exclude_episodes = exclude_episodes self.exclude_episodes = exclude_episodes
self.session = session or requests.Session() 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): def __getattr__(self, name):
if name in _SIMPLE_GET_ENDPOINTS: if name in _SIMPLE_GET_ENDPOINTS:

12
sickbeard/__init__.py

@ -146,7 +146,7 @@ CPU_PRESET = 'DISABLED'
ANON_REDIRECT = None ANON_REDIRECT = None
USE_API = False USE_API = False
API_KEY = None API_KEYS = []
ENABLE_HTTPS = False ENABLE_HTTPS = False
HTTPS_CERT = None HTTPS_CERT = None
@ -619,7 +619,7 @@ def init_stage_1(console_logging):
global THEME_NAME, DEFAULT_HOME, FANART_LIMIT, SHOWLIST_TAGVIEW, SHOW_TAGS, \ 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, \ 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, \ 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, \ ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY, \
SEND_SECURITY_HEADERS, ALLOWED_HOSTS SEND_SECURITY_HEADERS, ALLOWED_HOSTS
# Gen Config/Advanced # 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)) TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0))
USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 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)) 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']['cpu_preset'] = CPU_PRESET
new_config['General']['anon_redirect'] = ANON_REDIRECT new_config['General']['anon_redirect'] = ANON_REDIRECT
new_config['General']['use_api'] = int(USE_API) 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']['debug'] = int(DEBUG)
new_config['General']['enable_https'] = int(ENABLE_HTTPS) new_config['General']['enable_https'] = int(ENABLE_HTTPS)
new_config['General']['https_cert'] = HTTPS_CERT new_config['General']['https_cert'] = HTTPS_CERT

5
sickbeard/indexermapper.py

@ -17,6 +17,7 @@
import datetime import datetime
import re import re
import traceback import traceback
import os
import requests import requests
import sickbeard import sickbeard
@ -26,7 +27,7 @@ from lib.dateutil.parser import parse
from lib.unidecode import unidecode from lib.unidecode import unidecode
from libtrakt import TraktAPI from libtrakt import TraktAPI
from libtrakt.exceptions import TraktAuthException, TraktException 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.helpers import tryInt, getURL
from sickbeard.indexers.indexer_config import (INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TVMAZE, from sickbeard.indexers.indexer_config import (INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TVMAZE,
INDEXER_IMDB, INDEXER_TRAKT, INDEXER_TMDB) INDEXER_IMDB, INDEXER_TRAKT, INDEXER_TMDB)
@ -198,7 +199,7 @@ def get_trakt_ids(url_trakt):
def get_imdbid_by_name(name, startyear): def get_imdbid_by_name(name, startyear):
ids = {} ids = {}
try: 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: for r in res:
if isinstance(r.get('type'), basestring) and 'tv series' == r.get('type').lower() \ if isinstance(r.get('type'), basestring) and 'tv series' == r.get('type').lower() \
and str(startyear) == str(r.get('year')): and str(startyear) == str(r.get('year')):

2
sickbeard/tv.py

@ -1116,7 +1116,7 @@ class TVShow(object):
self._imdbid = redirect_check self._imdbid = redirect_check
imdb_id = redirect_check imdb_id = redirect_check
imdb_info['imdb_id'] = self.imdbid 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): 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) logger.log('Not a valid imdbid: %s for show: %s' % (imdb_id, self.name), logger.WARNING)
return 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 common import Quality, qualityPresetStrings, statusStrings
from sickbeard.indexers.indexer_config import * from sickbeard.indexers.indexer_config import *
from sickbeard.indexers import indexer_config, indexer_api from sickbeard.indexers import indexer_config, indexer_api
from sickbeard.tv import TVShow, TVEpisode
from tornado import gen from tornado import gen
from sickbeard.search_backlog import FORCED_BACKLOG from sickbeard.search_backlog import FORCED_BACKLOG
from sickbeard.webserve import NewHomeAddShows from sickbeard.webserve import NewHomeAddShows
from six import iteritems, integer_types
try: try:
import json import json
@ -92,6 +94,13 @@ quality_map = {'sdtv': Quality.SDTV,
quality_map_inversed = {v: k for k, v in quality_map.iteritems()} 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): class ApiServerLoading(webserve.BaseHandler):
@gen.coroutine @gen.coroutine
def get(self, route, *args, **kwargs): def get(self, route, *args, **kwargs):
@ -104,12 +113,16 @@ class PythonObjectEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, set): if isinstance(obj, set):
return list(obj) 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) return json.JSONEncoder.default(self, obj)
class Api(webserve.BaseHandler): class Api(webserve.BaseHandler):
""" api class that returns json results """ """ 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 intent = 4
def check_xsrf_cookie(self): def check_xsrf_cookie(self):
@ -147,8 +160,8 @@ class Api(webserve.BaseHandler):
args = args[1:] args = args[1:]
self.apiKey = sickbeard.API_KEY self.apiKeys = sickbeard.API_KEYS
access, accessMsg, args, kwargs = self._grand_access(self.apiKey, route, args, kwargs) access, accessMsg, args, kwargs = self._grand_access(self.apiKeys, route, args, kwargs)
# set the output callback # set the output callback
# default json # default json
@ -158,9 +171,9 @@ class Api(webserve.BaseHandler):
# do we have acces ? # do we have acces ?
if access: if access:
logger.log(accessMsg, logger.DEBUG) api_log(self, accessMsg, logger.DEBUG)
else: else:
logger.log(accessMsg, logger.WARNING) api_log(self, accessMsg, logger.WARNING)
return outputCallbackDict['default'](_responds(RESULT_DENIED, msg=accessMsg)) return outputCallbackDict['default'](_responds(RESULT_DENIED, msg=accessMsg))
# set the original call_dispatcher as the local _call_dispatcher # set the original call_dispatcher as the local _call_dispatcher
@ -181,7 +194,7 @@ class Api(webserve.BaseHandler):
try: try:
outDict = _call_dispatcher(self, args, kwargs) outDict = _call_dispatcher(self, args, kwargs)
except Exception as e: # real internal error oohhh nooo :( 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), errorData = {"error_msg": ex(e),
"args": args, "args": args,
"kwargs": kwargs} "kwargs": kwargs}
@ -203,28 +216,30 @@ class Api(webserve.BaseHandler):
out = '%s(%s);' % (callback, out) # wrap with JSONP call if requested 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 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( out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex(
e) + '"}' e) + '"}'
return out return out
def _grand_access(self, realKey, apiKey, args, kwargs): def _grand_access(self, realKeys, apiKey, args, kwargs):
""" validate api key and log result """ """ validate api key and log result """
remoteIp = self.request.remote_ip remoteIp = self.request.remote_ip
self.apikey_name = ''
if not sickbeard.USE_API: if not sickbeard.USE_API:
msg = u'API :: ' + remoteIp + ' - SB API Disabled. ACCESS DENIED' msg = u'%s - SB API Disabled. ACCESS DENIED' % remoteIp
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'
return False, msg, args, kwargs return False, msg, args, kwargs
else: if not apiKey:
msg = u'API :: ' + remoteIp + ' - gave WRONG API KEY ' + apiKey + '. ACCESS DENIED' msg = u'%s - gave NO API KEY. ACCESS DENIED' % remoteIp
return False, msg, args, kwargs 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): 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 calls the TVDBShorthandWrapper when the first args element is a number
or returns an error that there is no such cmd 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 cmds = None
if args: if args:
cmds = args[0] cmds = args[0]
@ -246,6 +257,12 @@ def call_dispatcher(handler, args, kwargs):
cmds = kwargs["cmd"] cmds = kwargs["cmd"]
del 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 = {} outDict = {}
if cmds != None: if cmds != None:
cmds = cmds.split("|") 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 ? 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 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 if not (multiCmds and cmd in ('show.getposter', 'show.getbanner')): # skip these cmd while chaining
try: try:
if cmd in _functionMaper: if cmd in _functionMaper:
@ -346,6 +363,12 @@ class ApiCall(object):
# old sickbeard call # old sickbeard call
self._sickbeard_call = getattr(self, '_sickbeard_call', False) 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 @property
def sickbeard_call(self): def sickbeard_call(self):
@ -361,6 +384,24 @@ class ApiCall(object):
# override with real output function in subclass # override with real output function in subclass
return {} 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): def return_help(self):
try: try:
if self._requiredParams: if self._requiredParams:
@ -403,6 +444,13 @@ class ApiCall(object):
msg = "The required parameter: '" + self._missing[0] + "' was not set" msg = "The required parameter: '" + self._missing[0] + "' was not set"
else: else:
msg = "The required parameters: '" + "','".join(self._missing) + "' where not set" 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) return _responds(RESULT_ERROR, msg=msg)
def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None): def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None):
@ -516,7 +564,7 @@ class ApiCall(object):
elif type == "ignore": elif type == "ignore":
pass pass
else: 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) logger.ERROR)
if error: if error:
@ -765,13 +813,14 @@ class CMD_ListCommands(ApiCall):
color = ("", " style='color: grey !important;'")[is_old_command] 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]) 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): if isinstance(help, dict):
sg_c = '' sg_cmd_new = self._get_old_command(command_class=v)
if "SickGearCommand" in help: sg_cmd = ''
sg_c += '<td>%s</td>' % help['SickGearCommand'] if sg_cmd_new:
out += "<p style='color: darkgreen !important;'>for all features use SickGear API Command: <b>%s</b></p>" % help['SickGearCommand'] 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 "desc" in help:
if is_old_command: 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: else:
table_sickgear_commands += '<td>%s</td>' % help['desc'] table_sickgear_commands += '<td>%s</td>' % help['desc']
out += help['desc'] out += help['desc']
@ -832,7 +881,7 @@ class CMD_Help(ApiCall):
def run(self): def run(self):
""" display help information for a given subject/command """ """ display help information for a given subject/command """
if self.subject in _functionMaper: 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: else:
out = _responds(RESULT_FAILURE, msg="No such cmd") out = _responds(RESULT_FAILURE, msg="No such cmd")
return out return out
@ -1302,7 +1351,7 @@ class CMD_SickGearEpisodeSetStatus(ApiCall):
cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment) cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment)
sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable 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") season) + " because some episodes were set to WANTED")
extra_msg = " Backlog started" extra_msg = " Backlog started"
@ -2051,11 +2100,15 @@ class CMD_SickGearForceSearch(ApiCall):
def run(self): def run(self):
""" force the given search type search """ """ force the given search type search """
result = None 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() result = sickbeard.recentSearchScheduler.forceRun()
elif 'backlog' == self.searchtype: elif 'backlog' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_backlog_in_progress() \
result = sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG) and not sickbeard.backlogSearchScheduler.action.amActive:
elif 'proper' == self.searchtype: 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() result = sickbeard.properFinderScheduler.forceRun()
if result: if result:
return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype) 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(), data = {"status": statusStrings[sickbeard.STATUS_DEFAULT].lower(),
"flatten_folders": int(sickbeard.FLATTEN_FOLDERS_DEFAULT), "initial": anyQualities, "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) return _responds(RESULT_SUCCESS, data)
@ -2470,12 +2523,12 @@ class CMD_SickGearSearchIndexers(ApiCall):
try: try:
myShow = t[int(self.indexerid), False] myShow = t[int(self.indexerid), False]
except (sickbeard.indexer_shownotfound, sickbeard.indexer_error): 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}) return _responds(RESULT_SUCCESS, {"results": [], "langid": lang_id})
if not myShow.data['seriesname']: if not myShow.data['seriesname']:
logger.log( self.log(
u"API :: Found show with indexerid " + str(self.indexerid) + ", however it contained no show name", u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name",
logger.DEBUG) logger.DEBUG)
return _responds(RESULT_FAILURE, msg="Show contains no name, invalid result") 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) CMD_SickGearSearchIndexers.__init__(self, handler, args, kwargs)
class CMD_SickBeardSetDefaults(ApiCall): class CMD_SickGearSetDefaults(ApiCall):
_help = {"desc": "set sickgear user defaults", _help = {"desc": "set sickgear user defaults",
"optionalParameters": {"initial": {"desc": "initial quality for the show"}, "optionalParameters": {"initial": {"desc": "initial quality for the show"},
"archive": {"desc": "archive 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") 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): class CMD_SickGearSetSceneNumber(ApiCall):
_help = {"desc": "set Scene Numbers", _help = {"desc": "set Scene Numbers",
"requiredParameters": {"indexerid": {"desc": "unique id of a show"}, "requiredParameters": {"indexerid": {"desc": "unique id of a show"},
@ -2647,7 +2716,7 @@ class CMD_SickGearActivateSceneNumber(ApiCall):
msg="Scene Numbering %sactivated" % ('de', '')[self.activate]) msg="Scene Numbering %sactivated" % ('de', '')[self.activate])
class CMD_SickBeardShutdown(ApiCall): class CMD_SickGearShutdown(ApiCall):
_help = {"desc": "shutdown sickgear"} _help = {"desc": "shutdown sickgear"}
def __init__(self, handler, args, kwargs): def __init__(self, handler, args, kwargs):
@ -2662,6 +2731,16 @@ class CMD_SickBeardShutdown(ApiCall):
return _responds(RESULT_SUCCESS, msg="SickGear is shutting down...") 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): class CMD_SickGearListIgnoreWords(ApiCall):
_help = {"desc": "list ignore words", _help = {"desc": "list ignore words",
"optionalParameters": {"indexerid": {"desc": "unique id of a show"}, "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
@ -2959,8 +3038,11 @@ class CMD_SickGearShow(ApiCall):
return _responds(RESULT_FAILURE, msg="Show not found") return _responds(RESULT_FAILURE, msg="Show not found")
showDict = {} showDict = {}
showDict["season_list"] = CMD_ShowSeasonList(self.handler, (), {"indexerid": self.indexerid}).run()["data"] showDict["season_list"] = CMD_SickGearShowSeasonList(self.handler, (),
showDict["cache"] = CMD_ShowCache(self.handler, (), {"indexerid": self.indexerid}).run()["data"] {"indexer": self.indexer, "indexerid": self.indexerid}
).run()["data"]
showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": self.indexer,
"indexerid": self.indexerid}).run()["data"]
genreList = [] genreList = []
if showObj.genre: if showObj.genre:
@ -3083,15 +3165,28 @@ class CMD_SickGearShowAddExisting(ApiCall):
if not ek.ek(os.path.isdir, self.location): if not ek.ek(os.path.isdir, self.location):
return _responds(RESULT_FAILURE, msg='Not a valid 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 indexerName = None
indexerResult = CMD_SickBeardSearchIndexers(self.handler, [], if not myShow.data['seriesname']:
{"indexerid": self.indexerid, "indexer": self.indexer}).run() 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]: else:
if not indexerResult['data']['results']: indexerName = myShow.data['seriesname']
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']
if not indexerName: if not indexerName:
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") 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): def __init__(self, handler, args, kwargs):
kwargs['indexer'] = INDEXER_TVDB kwargs['indexer'] = INDEXER_TVDB
# required # 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 # super, missing, help
self.sickbeard_call = True self.sickbeard_call = True
CMD_SickGearShowAddExisting.__init__(self, handler, args, kwargs) CMD_SickGearShowAddExisting.__init__(self, handler, args, kwargs)
@ -3231,15 +3327,28 @@ class CMD_SickGearShowAddNew(ApiCall):
return _responds(RESULT_FAILURE, msg="Status prohibited") return _responds(RESULT_FAILURE, msg="Status prohibited")
newStatus = self.status 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 indexerName = None
indexerResult = CMD_SickBeardSearchIndexers(self.handler, [], if not myShow.data['seriesname']:
{"indexerid": self.indexerid, "indexer": self.indexer}).run() 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]: else:
if not indexerResult['data']['results']: indexerName = myShow.data['seriesname']
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']
if not indexerName: if not indexerName:
return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") 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 # don't create show dir if config says not to
if sickbeard.ADD_SHOWS_WO_DIR: 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: else:
dir_exists = helpers.makeDir(showPath) dir_exists = helpers.makeDir(showPath)
if not dir_exists: 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}, return _responds(RESULT_FAILURE, {"path": showPath},
"Unable to create the folder " + showPath + ", can't add the show") "Unable to create the folder " + showPath + ", can't add the show")
else: else:
@ -3292,7 +3401,8 @@ class CMD_ShowAddNew(CMD_SickGearShowAddNew):
def __init__(self, handler, args, kwargs): def __init__(self, handler, args, kwargs):
kwargs['indexer'] = INDEXER_TVDB 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 # required
# optional # optional
# super, missing, help # super, missing, help
@ -3359,6 +3469,7 @@ class CMD_SickGearShowDelete(ApiCall):
"requiredParameters": {"indexer": {"desc": "indexer of a show"}, "requiredParameters": {"indexer": {"desc": "indexer of a show"},
"indexerid": {"desc": "unique id of a show"}, "indexerid": {"desc": "unique id of a show"},
}, },
"optionalParameters": {"full": {"desc": "delete files/folder of show"}}
} }
def __init__(self, handler, args, kwargs): def __init__(self, handler, args, kwargs):
@ -3367,6 +3478,7 @@ class CMD_SickGearShowDelete(ApiCall):
[i for i in indexer_api.indexerApi().indexers]) [i for i in indexer_api.indexerApi().indexers])
self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
# optional # optional
self.full_delete, args = self.check_params(args, kwargs, "full", False, False, "bool", [])
# super, missing, help # super, missing, help
ApiCall.__init__(self, handler, args, kwargs) ApiCall.__init__(self, handler, args, kwargs)
@ -3380,7 +3492,7 @@ class CMD_SickGearShowDelete(ApiCall):
showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable
return _responds(RESULT_FAILURE, msg="Show can not be deleted while being added or updated") 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") 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 sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable
return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has queued to be updated") return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has queued to be updated")
except exceptions.CantUpdateException as e: 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)) return _responds(RESULT_FAILURE, msg="Unable to update " + str(showObj.name))
@ -4230,7 +4342,8 @@ class CMD_SickGearShows(ApiCall):
else: else:
showDict['next_ep_airdate'] = '' 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"]: if not showDict["network"]:
showDict["network"] = "" showDict["network"] = ""
if self.sort == "name": if self.sort == "name":
@ -4459,12 +4572,14 @@ _functionMaper = {"help": CMD_Help,
"sb.searchtvdb": CMD_SickBeardSearchIndexers, "sb.searchtvdb": CMD_SickBeardSearchIndexers,
"sg.searchtv": CMD_SickGearSearchIndexers, "sg.searchtv": CMD_SickGearSearchIndexers,
"sb.setdefaults": CMD_SickBeardSetDefaults, "sb.setdefaults": CMD_SickBeardSetDefaults,
"sg.setdefaults": CMD_SickGearSetDefaults,
"sg.setscenenumber": CMD_SickGearSetSceneNumber, "sg.setscenenumber": CMD_SickGearSetSceneNumber,
"sg.activatescenenumbering": CMD_SickGearActivateSceneNumber, "sg.activatescenenumbering": CMD_SickGearActivateSceneNumber,
"sg.getindexers": CMD_SickGearGetIndexers, "sg.getindexers": CMD_SickGearGetIndexers,
"sg.getindexericon": CMD_SickGearGetIndexerIcon, "sg.getindexericon": CMD_SickGearGetIndexerIcon,
"sg.getnetworkicon": CMD_SickGearGetNetworkIcon, "sg.getnetworkicon": CMD_SickGearGetNetworkIcon,
"sb.shutdown": CMD_SickBeardShutdown, "sb.shutdown": CMD_SickBeardShutdown,
"sg.shutdown": CMD_SickGearShutdown,
"sg.listignorewords": CMD_SickGearListIgnoreWords, "sg.listignorewords": CMD_SickGearListIgnoreWords,
"sg.setignorewords": CMD_SickGearSetIgnoreWords, "sg.setignorewords": CMD_SickGearSetIgnoreWords,
"sg.listrequiredwords": CMD_SickGearListRequireWords, "sg.listrequiredwords": CMD_SickGearListRequireWords,
@ -4512,3 +4627,5 @@ _functionMaper = {"help": CMD_Help,
"sg.shows.forceupdate": CMD_SickGearShowsForceUpdate, "sg.shows.forceupdate": CMD_SickGearShowsForceUpdate,
"sg.shows.queue": CMD_SickGearShowsQueue, "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 t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers
if sickbeard.indexerApi(i).config['active']]) if sickbeard.indexerApi(i).config['active']])
t.request_host = escape.xhtml_escape(self.request.host_name) 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() return t.respond()
def saveRootDirs(self, rootDirString=None): def saveRootDirs(self, rootDirString=None):
@ -5891,7 +5893,8 @@ class ConfigGeneral(Config):
sickbeard.save_config() sickbeard.save_config()
def generateKey(self, *args, **kwargs): @staticmethod
def generateKey(*args, **kwargs):
""" Return a new randomized API_KEY """ Return a new randomized API_KEY
""" """
@ -5911,20 +5914,60 @@ class ConfigGeneral(Config):
m.update(r) m.update(r)
# Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b # 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() 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = [] results = []
@ -6000,7 +6043,6 @@ class ConfigGeneral(Config):
sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected) sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected)
sickbeard.USE_API = config.checkbox_to_value(use_api) sickbeard.USE_API = config.checkbox_to_value(use_api)
sickbeard.API_KEY = api_key
sickbeard.WEB_PORT = config.to_int(web_port) sickbeard.WEB_PORT = config.to_int(web_port)
# sickbeard.WEB_LOG is set in config.change_log_dir() # sickbeard.WEB_LOG is set in config.change_log_dir()
@ -7313,8 +7355,9 @@ class ApiBuilder(MainHandler):
t.indexers = sickbeard.indexerApi().all_indexers t.indexers = sickbeard.indexerApi().all_indexers
t.searchindexers = sickbeard.indexerApi().search_indexers t.searchindexers = sickbeard.indexerApi().search_indexers
if len(sickbeard.API_KEY) == 32: if len(sickbeard.API_KEYS):
t.apikey = sickbeard.API_KEY # use first APIKEY for apibuilder tests
t.apikey = sickbeard.API_KEYS[0][1]
else: else:
t.apikey = 'api key not generated' t.apikey = 'api key not generated'

Loading…
Cancel
Save