# Author: Dennis Lutter <lad1337@gmail.com>
# Author: Jonathon Saine <thezoggy@gmail.com>
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear.  If not, see <http://www.gnu.org/licenses/>.

# noinspection PyProtectedMember
from mimetypes import MimeTypes
from random import randint

import datetime
import glob
import copy
import os
import re
import time
import traceback
from . import webserve

import exceptions_helper
from exceptions_helper import ex
from json_helper import is_orjson, json_dumps, JSON_INDENT, json_loads, JSONEncoder, ORJSON_OPTIONS
from tornado import gen
from tornado.concurrent import run_on_executor
from lib import subliminal

import sickgear
from . import classes, db, helpers, history, image_cache, logger, network_timezones, processTV, search_queue, ui
from .common import ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED, SNATCHED_ANY, SNATCHED_BEST, \
    SNATCHED_PROPER, UNAIRED, UNKNOWN, WANTED, Quality, qualityPresetStrings, statusStrings
from .name_parser.parser import NameParser
from .helpers import df, find_mount_point, starify
from .indexers import indexer_api, indexer_config
from .indexers.indexer_config import *
from lib.tvinfo_base.exceptions import *
from .scene_numbering import set_scene_numbering_helper
from .scheduler import Scheduler
from .search_backlog import FORCED_BACKLOG
from .show_updater import clean_ignore_require_words
from .sgdatetime import SGDatetime
from .tv import TVEpisode, TVShow,  TVidProdid
from .webserve import AddShows
import dateutil.parser

from _23 import decode_str, unquote_plus
from six import integer_types, iteritems, iterkeys, string_types, text_type

# noinspection PyUnreachableCode
if False:
    from typing import AnyStr, Dict, List, Tuple


dateFormat = "%Y-%m-%d"
dateTimeFormat = "%Y-%m-%d %H:%M"
timeFormat = '%A %I:%M %p'

RESULT_SUCCESS = 10  # only use inside the run methods
RESULT_FAILURE = 20  # only use inside the run methods
RESULT_TIMEOUT = 30  # not used yet :(
RESULT_ERROR = 40  # only use outside of the run methods !
RESULT_FATAL = 50  # only use in Api.default() ! this is the "we encountered an internal error" error
RESULT_DENIED = 60  # only use in Api.default() ! this is the access denied error
result_type_map = {RESULT_SUCCESS: "success",
                   RESULT_FAILURE: "failure",
                   RESULT_TIMEOUT: "timeout",
                   RESULT_ERROR: "error",
                   RESULT_FATAL: "fatal",
                   RESULT_DENIED: "denied",
                   }
# basically everything except RESULT_SUCCESS / success is bad

quality_map = {'sdtv': Quality.SDTV,
               'sddvd': Quality.SDDVD,
               'hdtv': Quality.HDTV,
               'rawhdtv': Quality.RAWHDTV,
               'fullhdtv': Quality.FULLHDTV,
               'hdwebdl': Quality.HDWEBDL,
               'fullhdwebdl': Quality.FULLHDWEBDL,
               'hdbluray': Quality.HDBLURAY,
               'fullhdbluray': Quality.FULLHDBLURAY,
               'uhd4kweb': Quality.UHD4KWEB,
               'unknown': Quality.UNKNOWN}

quality_map_inversed = {v: k for k, v in iteritems(quality_map)}


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):
        self.finish(json_dumps({'error_msg': 'Server is loading'}))

    post = get


if is_orjson:

    def orjson_default(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.tvid, 'indexer_id': obj.prodid}
        elif isinstance(obj, datetime.datetime):
            return SGDatetime.sbfdatetime(obj, d_preset=dateFormat, t_preset='%H:%M %z')
        elif isinstance(obj, datetime.date):
            return SGDatetime.sbfdate(obj, d_preset=dateFormat)
        raise TypeError

    json_enc_kw = {'default': orjson_default, 'option': ORJSON_OPTIONS}

else:

    class PythonObjectEncoder(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.tvid, 'indexer_id': obj.prodid}
            elif isinstance(obj, datetime.datetime):
                return SGDatetime.sbfdatetime(obj, d_preset=dateFormat, t_preset='%H:%M %z')
            elif isinstance(obj, datetime.date):
                return SGDatetime.sbfdate(obj, d_preset=dateFormat)
            return JSONEncoder.default(self, obj)

    json_enc_kw = {'cls': PythonObjectEncoder, 'indent': JSON_INDENT, 'sort_keys': True}


class Api(webserve.BaseHandler):
    """ api class that returns json results """
    version = 15  # use an int since float-point is unpredictable

    def check_xsrf_cookie(self):
        pass

    def set_default_headers(self):
        self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        self.set_header('X-Robots-Tag', 'noindex, nofollow, noarchive, nocache, noodp, noydir, noimageindex, nosnippet')
        if sickgear.SEND_SECURITY_HEADERS:
            self.set_header('X-Frame-Options', 'SAMEORIGIN')
        self.set_header('X-Application', 'SickGear')
        self.set_header('X-API-Version', Api.version)

    def prepare(self):
        # Incorporate request JSON into arguments dictionary.
        if self.request.body:
            try:
                json_data = {'payloadjson': json_loads(self.request.body)}
                self.request.arguments.update(json_data)
            except (BaseException, Exception):
                raise ApiError('Unable to parse JSON.')
        super(Api, self).prepare()

    def post(self, route, *args, **kwargs):
        return self.get(route, *args, **kwargs)

    def _decode_params(self, kwargs):
        for k, v in iteritems(kwargs):
            if isinstance(v, list):
                kwargs[k] = [decode_str(l) for l in v]
            elif isinstance(v, dict):
                kwargs[k] = {a: decode_str(b) for a, b in iteritems(v)}
            else:
                kwargs[k] = decode_str(v)
        return kwargs

    @gen.coroutine
    def get(self, route, *args, **kwargs):
        route = route.strip('/') or 'index'

        kwargs = self._decode_params(self.request.arguments)
        for arg, value in iteritems(kwargs):
            if not isinstance(value, dict) and 1 == len(value):
                kwargs[arg] = value[0]

        args = args[1:]

        self.apiKeys = sickgear.API_KEYS
        access, accessMsg, args, kwargs = self._grant_access(self.apiKeys, route, args, kwargs)

        # set the output callback
        # default json
        outputCallbackDict = {'default': self._out_as_json,
                              'image': lambda x: x['image'],
                              'raw': lambda x: x['raw']
                              }

        # do we have access ?
        if access:
            api_log(self, accessMsg, logger.DEBUG)
        else:
            api_log(self, accessMsg, logger.WARNING)
            result = yield self.async_call(outputCallbackDict['default'], (_responds(RESULT_DENIED, msg=accessMsg), ))
            self.finish(result)
            return

        # set the original call_dispatcher as the local _call_dispatcher
        _call_dispatcher = call_dispatcher
        # if profile was set wrap "_call_dispatcher" in the profile function
        if 'profile' in kwargs:
            from lib.profilehooks import profile

            _call_dispatcher = profile(_call_dispatcher, immediate=True)
            del kwargs["profile"]

        # if debug was set call the "_call_dispatcher"
        if 'debug' in kwargs:
            outDict = _call_dispatcher(self, args,
                                       kwargs)  # this way we can debug the cherry.py traceback in the browser
            del kwargs["debug"]
        else:  # if debug was not set we wrap the "call_dispatcher" in a try block to assure a json output
            try:
                outDict = yield self.async_call(_call_dispatcher, (self, args, kwargs))
            except Exception as e:  # real internal error
                api_log(self, ex(e), logger.ERROR)
                errorData = {"error_msg": ex(e),
                             "args": args,
                             "kwargs": kwargs}
                outDict = _responds(RESULT_FATAL, errorData,
                                    "SickGear encountered an internal error! Please report to the Devs")

        if 'outputType' in outDict:
            outputCallback = outputCallbackDict[outDict['outputType']]
        else:
            outputCallback = outputCallbackDict['default']
        self.finish(outputCallback(outDict))

    @run_on_executor
    def async_call(self, function, ag):
        try:
            result = function(*ag)
            return result
        except Exception as e:
            logger.error(ex(e))
            raise e

    def _out_as_json(self, dict):
        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        try:
            out = json_dumps(dict, **json_enc_kw)
            callback = self.get_query_argument('callback', None) or self.get_query_argument('jsonp', None)
            if None is not callback:
                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
            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 _grant_access(self, realKeys, apiKey, args, kwargs):
        """ validate api key and log result """
        remoteIp = self.request.remote_ip
        self.apikey_name = ''
        self.api_access_granted = False  # type: bool

        if not sickgear.USE_API:
            msg = f'{remoteIp} - SB API Disabled. ACCESS DENIED'
            return False, msg, args, kwargs
        if not apiKey:
            msg = f'{remoteIp} - gave NO API KEY. ACCESS DENIED'
            return False, msg, args, kwargs
        for realKey in realKeys:
            if apiKey == realKey[1]:
                self.apikey_name = realKey[0]
                self.api_access_granted = True
                msg = f'{remoteIp} - gave correct API KEY: {realKey[0]}. ACCESS GRANTED'
                return True, msg, args, kwargs
        msg = f'{remoteIp} - gave WRONG API KEY {apiKey}. ACCESS DENIED'
        return False, msg, args, kwargs


def call_dispatcher(handler, args, kwargs):
    """ calls the appropriate CMD class
        looks for a cmd in args and kwargs
        or calls the TVDBShorthandWrapper when the first args element is a number
        or returns an error that there is no such cmd
    """
    cmds = None
    if args:
        cmds = args[0]
        args = args[1:]

    if "cmd" in kwargs:
        cmds = kwargs["cmd"]
        del kwargs["cmd"]

    api_log(handler, f'cmd: "{cmds}"', logger.DEBUG)
    api_log(handler, f'all args: "{args}"', logger.DEBUG)
    api_log(handler, f'all kwargs: "{kwargs}"', logger.DEBUG)
    # logger.debug(f'dateFormat: "{dateFormat}"')


    outDict = {}
    if None is not cmds:
        cmds = cmds.split("|")
        multiCmds = bool(1 < len(cmds))
        for cmd in cmds:
            curArgs, curKwargs = filter_params(cmd, args, kwargs)
            cmdIndex = None
            if 1 < len(cmd.split("_")):  # was a index used for this cmd ?
                cmd, cmdIndex = cmd.split("_")  # this gives us the clear cmd and the index

            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:
                        curOutDict = _functionMaper.get(cmd)(handler, curArgs,
                                                             curKwargs).run()  # get the cmd class, init it and run()
                    elif _is_int(cmd):
                        curOutDict = TVDBShorthandWrapper(handler, curArgs, curKwargs, cmd).run()
                    else:
                        curOutDict = _responds(RESULT_ERROR, "No such cmd: '" + cmd + "'")
                except ApiError as e:  # Api errors that we raised, they are harmless
                    curOutDict = _responds(RESULT_ERROR, msg=ex(e))
            else:  # if someone chained one of the forbidden cmds they will get an error for this one cmd
                curOutDict = _responds(RESULT_ERROR, msg="The cmd '" + cmd + "' is not supported while chaining")

            if multiCmds:
                # note: if multiple same cmds are issued but one has not an index defined it will override all others
                # or the other way around, this depends on the order of the cmds
                # this is not a bug
                if None is cmdIndex:  # do we need a index dict for this cmd ?
                    outDict[cmd] = curOutDict
                else:
                    if cmd not in outDict:
                        outDict[cmd] = {}
                    outDict[cmd][cmdIndex] = curOutDict
            else:
                outDict = curOutDict

        if multiCmds:  # if we had multiple cmds we have to wrap it in a response dict
            outDict = _responds(RESULT_SUCCESS, outDict)
    else:  # index / no cmd given
        outDict = CMD_SickBeard(handler, args, kwargs).run()

    return outDict


def filter_params(cmd, args, kwargs):
    # type: (AnyStr, List, Dict) -> Tuple[List, Dict]
    """ return only params kwargs that are for cmd
        and rename them to a clean version (remove "<cmd>_")
        args are shared across all cmds

        all args and kwarks are lowerd

        cmd are separated by "|" e.g. &cmd=shows|future
        kwargs are namespaced with "." e.g. show.prodid=101501
        if a karg has no namespace asing it anyways (global)

        full e.g.
        /api?apikey=1234&cmd=show.seasonlist_asd|show.seasonlist_2&show.seasonlist_asd.prodid=101501&show.seasonlist_2.prodid=79488&sort=asc

        two calls of show.seasonlist
        one has the index "asd" the other one "2"
        the "indexerid" kwargs / params have the indexed cmd as a namspace
        and the kwarg / param "sort" is a used as a global
    """
    curArgs = []
    for arg in args:
        curArgs.append(arg.lower())
    curArgs = tuple(curArgs)

    curKwargs = {}
    for kwarg in kwargs:
        if 0 == kwarg.find(cmd + "."):
            cleanKey = kwarg.rpartition(".")[2]
            curKwargs[cleanKey] = kwargs[kwarg].lower()
        elif "." not in kwarg:  # the kwarg was not namespaced therefore a "global"
            curKwargs[kwarg] = kwargs[kwarg]
    return curArgs, curKwargs


class ApiCall(object):
    _help = {"desc": "No help message available. Please tell the devs that a help msg is missing for this cmd"}

    def __init__(self,
                 handler,
                 args,  # type: List
                 kwargs  # type: Dict
                 ):

        # missing
        try:
            if self._missing:
                self.run = self.return_missing
        except AttributeError:
            pass
        # help
        if 'help' in kwargs:
            self.run = self.return_help

        # RequestHandler
        self.handler = handler

        # 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)

        self._requiredParams = []
        self._optionalParams = []

    @property
    def sickbeard_call(self):
        if hasattr(self, '_sickbeard_call'):
            return self._sickbeard_call
        return False

    @sickbeard_call.setter
    def sickbeard_call(self, v):
        self._sickbeard_call = v

    def run(self):
        # override with real output function in subclass
        return {}

    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:
                pass
        except AttributeError:
            self._requiredParams = []
        try:
            if self._optionalParams:
                pass
        except AttributeError:
            self._optionalParams = []

        for paramDict, param_type in [
            (self._requiredParams, "requiredParameters"),
            (self._optionalParams, "optionalParameters")
        ]:

            if param_type in self._help:
                for param_name in paramDict:
                    if param_name not in self._help[param_type]:
                        self._help[param_type][param_name] = {}
                    if paramDict[param_name]["allowedValues"]:
                        self._help[param_type][param_name]["allowedValues"] = paramDict[param_name]["allowedValues"]
                    else:
                        self._help[param_type][param_name]["allowedValues"] = "see desc"
                    self._help[param_type][param_name]["defaultValue"] = paramDict[param_name]["defaultValue"]

            elif paramDict:
                for param_name in paramDict:
                    self._help[param_type] = {}
                    self._help[param_type][param_name] = paramDict[param_name]
            else:
                self._help[param_type] = {}
        msg = "No description available"
        if "desc" in self._help:
            msg = self._help["desc"]
            del self._help["desc"]
        return _responds(RESULT_SUCCESS, self._help, msg)

    def return_missing(self):
        if 1 == len(self._missing):
            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,  # type: List
                     kwargs,  # type: Dict
                     key,
                     default,
                     required,
                     type,
                     allowedValues,
                     sub_type=None
                     ):
        # TODO: explain this
        """ function to check passed params for the shorthand wrapper
            and to detect missing/required param
        """
        # Fix for applications that send tvdbid instead of indexerid
        if self.sickbeard_call and "indexerid" == key and "indexerid" not in kwargs:
            key = "tvdbid"

        missing = True
        orgDefault = default

        if "bool" == type:
            allowedValues = [0, 1]

        if args:
            default = args[0]
            missing = False
            args = args[1:]
        if kwargs.get(key):
            default = kwargs.get(key)
            missing = False
        if required:
            try:
                _ = self._missing
                self._requiredParams[key] = {"allowedValues": allowedValues, "defaultValue": orgDefault}
            except AttributeError:
                self._missing = []
                self._requiredParams = {key: {"allowedValues": allowedValues, "defaultValue": orgDefault}}
            if missing and key not in self._missing:
                self._missing.append(key)
        else:
            try:
                self._optionalParams[key] = {"allowedValues": allowedValues, "defaultValue": orgDefault}
            except AttributeError:
                self._optionalParams = {key: {"allowedValues": allowedValues, "defaultValue": orgDefault}}

        if default:
            default = self._check_param_type(default, key, type, sub_type)
            if "bool" == type:
                type = []
            self._check_param_value(default, key, allowedValues)

        return default, args

    def _check_param_type(self, value, name, type, sub_type):
        """ checks if value can be converted / parsed to type
            will raise an error on failure
            or will convert it to type and return new converted value
            can check for:
            - int: will be converted into int
            - bool: will be converted to False / True
            - list: will always return a list
            - string: will do nothing for now
            - ignore: will ignore it, just like "string"
        """
        error = False
        if "int" == type:
            if _is_int(value):
                value = int(value)
            else:
                error = True
        elif "bool" == type:
            if value in ("0", "1"):
                value = bool(int(value))
            elif value in ("true", "True", "TRUE"):
                value = True
            elif value in ("false", "False", "FALSE"):
                value = False
            else:
                error = True
        elif "list" == type:
            if None is not sub_type:
                if sub_type in integer_types:
                    if isinstance(value, integer_types):
                        value = [value]
                    elif isinstance(value, string_types):
                        if '|' in value:
                            li = [int(v) for v in value.split('|')]
                            if any(not isinstance(v, integer_types) for v in li):
                                error = True
                            else:
                                value = li
                        else:
                            value = [int(value)]
                    else:
                        error = True
                else:
                    li = value.split('|')
                    if any(sub_type is not type(v) for v in li):
                        error = True
                    else:
                        value = li
            else:
                value = value.split("|")
        elif "dict" == type:
            if isinstance(value, dict):
                value = value
            else:
                error = True
        elif "string" == type:
            pass
        elif "ignore" == type:
            pass
        else:
            self.log(f"Invalid param type set {type} can not check or convert ignoring it", logger.ERROR)

        if error:
            # this is a real ApiError !!
            raise ApiError(f'param: "{name}" with given value: "{value}" could not be parsed into "{type}"')

        return value

    def _check_param_value(self, value, name, allowedValues):
        """ will check if value (or all values in it ) are in allowed values
            will raise an exception if value is "out of range"
            if bool(allowedValue) == False a check is not performed and all values are excepted
        """
        if allowedValues:
            error = False
            if isinstance(value, list):
                for item in value:
                    if item not in allowedValues:
                        error = True
            else:
                if value not in allowedValues:
                    error = True

            if error:
                # this is kinda a ApiError but raising an error is the only way of quitting here
                raise ApiError(f'param: "{name}" with given value: "{value}" is out of allowed range "{allowedValues}"')


class TVDBShorthandWrapper(ApiCall):
    _help = {"desc": "this is an internal function wrapper. call the help command directly for more information"}

    def __init__(self,
                 handler,
                 args,  # type: List
                 kwargs,  # type: Dict
                 sid  # type: AnyStr
                 ):
        self.handler = handler
        self.origArgs = args
        self.kwargs = kwargs
        self.sid = sid

        self.s, args = self.check_params(args, kwargs, "s", None, False, "ignore", [])
        self.e, args = self.check_params(args, kwargs, "e", None, False, "ignore", [])
        self.args = args

        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ internal function wrapper """
        args = (self.sid,) + self.origArgs
        if self.e:
            return CMD_Episode(self.handler, args, self.kwargs).run()
        elif self.s:
            return CMD_ShowSeasons(self.handler, args, self.kwargs).run()
        else:
            return CMD_Show(self.handler, args, self.kwargs).run()


# ###############################
# helper functions         #
# ###############################

def _sizeof_fmt(num):
    for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
        if 1024.00 > num:
            return "%3.2f %s" % (num, x)
        num /= 1024.00


def _is_int(data):
    try:
        int(data)
    except (TypeError, ValueError, OverflowError):
        return False
    else:
        return True


def _rename_element(dict, oldKey, newKey):
    try:
        dict[newKey] = dict[oldKey]
        del dict[oldKey]
    except (ValueError, TypeError, NameError):
        pass
    return dict


def _responds(result_type, data=None, msg=""):
    """
    result is a string of given "type" (success/failure/timeout/error)
    message is a human readable string, can be empty
    data is either a dict or a array, can be a empty dict or empty array
    """
    if None is data:
        data = {}
    return {"result": result_type_map[result_type],
            "message": msg,
            "data": data}


def _get_quality_string(q):
    qualityString = "Custom"
    if q in qualityPresetStrings:
        qualityString = qualityPresetStrings[q]
    elif q in Quality.qualityStrings:
        qualityString = Quality.qualityStrings[q]
    return qualityString


def _get_status_Strings(s):
    return statusStrings[s]


def _ordinal_to_dateTimeForm(ordinal):
    # workaround for episodes with no airdate
    if 1 != int(ordinal):
        date = datetime.date.fromordinal(ordinal)
    else:
        return ""
    return date.strftime(dateTimeFormat)


def _ordinal_to_dateForm(ordinal):
    if 1 != int(ordinal):
        date = datetime.date.fromordinal(ordinal)
    else:
        return ""
    return date.strftime(dateFormat)


def _historyDate_to_dateTimeForm(timeString):
    date = datetime.datetime.strptime(timeString, history.dateFormat)
    return date.strftime(dateTimeFormat)


def _replace_statusStrings_with_statusCodes(statusStrings):
    statusCodes = []
    if "snatched" in statusStrings:
        statusCodes += Quality.SNATCHED
    if "downloaded" in statusStrings:
        statusCodes += Quality.DOWNLOADED
    if "skipped" in statusStrings:
        statusCodes.append(SKIPPED)
    if "wanted" in statusStrings:
        statusCodes.append(WANTED)
    if "archived" in statusStrings:
        statusCodes += Quality.ARCHIVED
    if "ignored" in statusStrings:
        statusCodes.append(IGNORED)
    if "unaired" in statusStrings:
        statusCodes.append(UNAIRED)
    return statusCodes


def _mapQuality(show_obj):
    quality_map = _getQualityMap()

    anyQualities = []
    bestQualities = []

    iqualityID, aqualityID = Quality.split_quality(int(show_obj))
    if iqualityID:
        for quality in iqualityID:
            anyQualities.append(quality_map[quality])
    if aqualityID:
        for quality in aqualityID:
            bestQualities.append(quality_map[quality])
    return anyQualities, bestQualities


def _getQualityMap():
    return quality_map_inversed


def _get_root_dirs(get_freespace=False):
    # type: (bool) -> List[Dict]
    """

    :param get_freespace: include disk free space info in response
    """
    dir_list = []
    if not sickgear.ROOT_DIRS:
        return dir_list

    root_dirs = sickgear.ROOT_DIRS.split('|')
    default_index = int(root_dirs.pop(0))

    if len(root_dirs) < default_index:
        return dir_list

    # clean up the list - replace %xx escapes by their single-character equivalent
    root_dirs = [unquote_plus(x) for x in root_dirs]

    default_dir = root_dirs[default_index]

    if root_dirs and get_freespace and sickgear.DISPLAY_FREESPACE:
        diskfree, _ = df()

    for cur_root_dir in root_dirs:
        try:
            os.listdir(cur_root_dir)
            valid = 1
        except (BaseException, Exception):
            valid = 0

        new_entry = {'valid': valid, 'location': cur_root_dir, 'default': int(cur_root_dir is default_dir)}

        if get_freespace:
            # noinspection PyUnboundLocalVariable
            new_entry['free_space'] = 'Required setting "Display freespace" is not enabled' \
                if not sickgear.DISPLAY_FREESPACE \
                else next((_space for _disk, _space in diskfree or [] if _disk == find_mount_point(cur_root_dir)), '')

        dir_list.append(new_entry)

    return dir_list


class ApiError(Exception):
    """Generic API error"""


class IntParseError(Exception):
    """A value could not be parsed into a int. But should be parsable to a int """


# -------------------------------------------------------------------------------------#

class CMD_ListCommands(ApiCall):
    _help = {"desc": "list help of all commands"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get help information for all commands """
        out = ''
        table_sickgear_commands = ''
        table_sickgear_commands = ''
        for f, v in sorted(iteritems(_functionMaper), key=lambda x: (re.sub(r'^s[bg]\.', '', x[0], flags=re.I),
                                                                     re.sub(r'^sg\.', '1', x[0], flags=re.I))):
            if 'listcommands' == f:
                continue
            help = getattr(v, '_help', None)
            is_old_command = isinstance(help, dict) and "SickGearCommand" in help
            if is_old_command:
                table_sickgear_commands += '<tr><td>%s</td>' % f
            else:
                table_sickgear_commands += '<tr><td>%s</td>' % f
            color = ("", " sb")[is_old_command]
            out += '<hr><h1 class="command%s">%s%s</h1>' \
                   % (color, f, ("",
                                 " <span>(Sickbeard compatibility command)</span>"
                                 )[is_old_command])
            if isinstance(help, dict):
                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 class='use-sg-api'>for all features use SickGear API Command: <b>%s</b></p>" % sg_cmd_new
                if "desc" in help:
                    if is_old_command:
                        table_sickgear_commands += '<td>%s</td>%s' % (help['desc'], sg_cmd)
                    else:
                        table_sickgear_commands += '<td>%s</td>' % help['desc']
                    out += help['desc']

                table = ''

                if "requiredParameters" in help and isinstance(help['requiredParameters'], dict):
                    for p, d in iteritems(help['requiredParameters']):
                        des = ''
                        if isinstance(d, dict) and 'desc' in d:
                            des = d.get('desc')
                        table += "<tr><td><span>%s <span class='parareq'>required</span></span></td>" \
                                 "<td><p>%s</p></td></tr>" % (p, des)

                if "optionalParameters" in help and isinstance(help['optionalParameters'], dict):
                    for p, d in iteritems(help['optionalParameters']):
                        des = ''
                        if isinstance(d, dict) and 'desc' in d:
                            des = d.get('desc')
                        table += "<tr><td><span>%s <span class='paraopt'>optional</span></span></td>" \
                                 "<td><p>%s</p></td></tr>" % (p, des)
                if table:
                    out += "<table class='sickbeardTable' cellspacing='0' border='1' cellpadding='0'><thead>" \
                           "<tr><th style='width: 25%'>Parameter</th><th>Description</th></tr>" \
                           "</thead><tbody>"
                    out += table
                    out += '</tbody></table>'
            else:
                if is_old_command:
                    table_sickgear_commands += '<td>%s</td><td></td>' % 'no description'
                else:
                    table_sickgear_commands += '<td>%s</td>' % 'no description'

            if is_old_command:
                table_sickgear_commands += '</tr>'
            else:
                table_sickgear_commands += '</tr>'

        if table_sickgear_commands:
            out = "<h1>SickBeard Commands (compatibility):</h1>" \
                  "<table class='sickbeardTable' cellspacing='0' border='1' cellpadding='0'><thead>" \
                  "<tr><th style='width: 25%'>Command</th><th>Description</th>" \
                  "<th style='width: 25%'>Replacement SickGear Command</th></tr>" \
                  "</thead><tbody>" + table_sickgear_commands + '</tbody></table>' + out

        if table_sickgear_commands:
            out = "<h1>SickGear Commands:</h1>" \
                  "<table class='sickbeardTable' cellspacing='0' border='1' cellpadding='0'><thead>" \
                  "<tr><th style='width: 25%'>Command</th><th>Description</th></tr>" \
                  "</thead><tbody>" + table_sickgear_commands + '</tbody></table>' + out

        return {'outputType': 'raw', 'raw': out}


class CMD_Help(ApiCall):
    _help = {"desc": "get help information for a given subject/command",
             "optionalParameters": {"subject": {"desc": "command - the top level command"}}}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.subject, args = self.check_params(args, kwargs, "subject", "help",
                                               False, "string", iterkeys(_functionMaper))
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get help information for a given subject/command """
        if self.subject in _functionMaper:
            out = _responds(RESULT_SUCCESS, _functionMaper.get(self.subject)(None, (), {"help": 1}).run())
        else:
            out = _responds(RESULT_FAILURE, msg="No such cmd")
        return out


class CMD_SickGearComingEpisodes(ApiCall):
    _help = {"desc": "get the daily schedule",
             "optionalParameters":
                 {"sort": {"desc": "change the sort order"},
                  "type": {"desc": "one or more of allowed values separated by |"},
                  "paused": {"desc": "0 to exclude paused shows, 1 to include them, "
                                     "2 to only view paused, or omitted to use the SG default"},
                  }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.sort, args = self.check_params(args, kwargs, "sort", "date", False, "string", ["date", "show", "network"])
        self.type, args = self.check_params(args, kwargs, "type", "today|missed|soon|later", False, "list",
                                            ["missed", "later", "today", "soon"])
        self.paused, args = self.check_params(args, kwargs, "paused", sickgear.EPISODE_VIEW_DISPLAY_PAUSED,
                                              False, "int", [0, 1, 2])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the daily schedule """
        sql_result, fanart, sorts, next_week_dt, today, next_week = webserve.MainHandler.get_daily_schedule()
        sql_result.sort(key=sorts[(self.sort, 'time')['date' == self.sort]])

        finalEpResults = {}

        # add all requested types or all
        for curType in self.type:
            finalEpResults[curType] = []

        for ep in sql_result:
            """
                Missed:   yesterday... (less than 1week)
                Today:    today
                Soon:     tomorrow till next week
                Later:    later than next week
            """

            if not ((int(ep['paused']) and self.paused) or (not int(ep['paused']) and 2 != self.paused)):
                continue

            ep['airdate'] = int(ep["airdate"])

            status = "soon"
            if ep["airdate"] < today:
                status = "missed"
            elif ep["airdate"] >= next_week:
                status = "later"
            elif today <= ep["airdate"] < next_week:
                if ep["airdate"] == today:
                    status = "today"
                else:
                    status = "soon"

            # skip unwanted
            if None is not self.type and status not in self.type:
                continue

            if not ep["network"]:
                ep["network"] = ""

            ep["quality"] = _get_quality_string(ep["quality"])
            # clean up Tvdb horrible airs field
            ep['airs'] = str(ep['airs']).replace('am', ' AM').replace('pm', ' PM').replace('  ', ' ')
            # start day of the week on 1 (monday)
            ep['weekday'] = 1 + datetime.date.fromordinal(ep['airdate']).weekday()
            ep['ep_name'] = ep['name']
            ep['ep_plot'] = ep['description']
            # add parsed_datetime to the dict
            ep['local_datetime'] = SGDatetime.sbstrftime(
                SGDatetime.convert_to_setting(ep['parsed_datetime'], force_local=True), dateTimeFormat)
            ep['status_str'] = statusStrings[ep['status']]
            ep['network'] = ep['episode_network'] or ep['network']
            ep['timezone'] = ep['ep_timezone'] or ep['show_timezone'] or ep['timezone'] or (
                    ep['network'] and network_timezones.get_network_timezone(ep['network'], return_name=True)[1])

            # remove all field we don't want for api response
            for cur_f in list(ep):
                if cur_f not in [  # fields to preserve
                    'absolute_number', 'air_by_date', 'airdate', 'airs', 'archive_firstmatch',
                    'classification', 'data_network', 'data_show_name',
                    'ep_name', 'ep_plot', 'episode', 'episode_id', 'genre',
                    'imdb_id', 'imdb_url', 'indexer', 'indexer_id', 'indexerid',
                    'lang', 'local_datetime', 'network', 'overview', 'parsed_datetime', 'paused', 'prod_id',
                    'quality', 'runtime', 'scene', 'scene_absolute_number', 'scene_episode', 'scene_season',
                    'season', 'show_id', 'show_name', 'show_network', 'show_status', 'showid', 'startyear',
                    'status', 'status_str', 'tag', 'timezone', 'trakt_watched', 'tv_id', 'tvid_prodid',
                    'version', 'weekday'
                ]:
                    del ep[cur_f]

            # Add tvdbid for backward compatibility
            try:
                show_obj = helpers.find_show_by_id({ep['tv_id']: ep['prod_id']})
                ep['tvdbid'] = show_obj.ids.get(TVINFO_TVDB, {'id': 0})['id']
                ep['ids'] = {k: v.get('id') for k, v in iteritems(show_obj.ids)}
            except (BaseException, Exception):
                ep['tvdbid'] = (None, ep['prod_id'])[TVINFO_TVDB == ep['tv_id']]
                ep['ids'] = None

            ep['airdate'] = SGDatetime.sbfdate(
                datetime.date.fromordinal(ep['airdate']), d_preset=dateFormat)
            ep['parsed_datetime'] = SGDatetime.sbfdatetime(ep['parsed_datetime'],
                                                           d_preset=dateFormat, t_preset='%H:%M %z')

            # TODO: check if this obsolete
            if status not in finalEpResults:
                finalEpResults[status] = []

            finalEpResults[status].append(ep)

        return _responds(RESULT_SUCCESS, finalEpResults)


class CMD_ComingEpisodes(CMD_SickGearComingEpisodes):
    _help = {"desc": "get the daily schedule",
             "optionalParameters":
                 {"sort": {"desc": "change the sort order"},
                  "type": {"desc": "one or more of allowed values separated by |"},
                  "paused": {"desc": "0 to exclude paused shows, 1 to include them, or omitted to use the SG default"}
                  },
             "SickGearCommand": "sg.future"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearComingEpisodes.__init__(self, handler, args, kwargs)


class CMD_SickGearEpisode(ApiCall):
    _help = {"desc": "get episode information",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             "optionalParameters": {"full_path": {
                 "desc": "show the full absolute path (if valid) instead of a relative path for the episode location"}},
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.s, args = self.check_params(args, kwargs, "season", None, True, "int", [])
        self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", [])
        # optional
        self.fullPath, args = self.check_params(args, kwargs, "full_path", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get episode information """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        my_db = db.DBConnection(row_type="dict")
        sql_result = my_db.select(
            "SELECT name, description, airdate, status, location, file_size, release_name, "
            " subtitles, absolute_number,scene_season, scene_episode, scene_absolute_number"
            " FROM tv_episodes"
            " WHERE indexer = ? AND showid = ?"
            " AND episode = ? AND season = ?",
            [self.tvid, self.prodid, self.e, self.s])
        if 1 != len(sql_result):
            raise ApiError("Episode not found")
        episode = sql_result[0]
        # handle path options
        # absolute vs relative vs broken
        showPath = None
        try:
            showPath = show_obj.location
        except exceptions_helper.ShowDirNotFoundException:
            pass

        if bool(self.fullPath) and showPath:
            pass
        elif not bool(self.fullPath) and showPath:
            # using the length because lstrip removes too much
            showPathLength = len(showPath) + 1  # the / or \ yeah not that nice i know
            episode["location"] = episode["location"][showPathLength:]
        elif not showPath:  # show dir is broken ... episode path will be empty
            episode["location"] = ""
        # convert stuff to human form
        timezone, episode['timezone'] = network_timezones.get_network_timezone(show_obj.network, return_name=True)
        episode['airdate'] = SGDatetime.sbfdate(SGDatetime.convert_to_setting(
            network_timezones.parse_date_time(int(episode['airdate']), show_obj.airs, timezone)), d_preset=dateFormat)
        status, quality = Quality.split_composite_status(int(episode["status"]))
        episode["status"] = _get_status_Strings(status)
        episode["quality"] = _get_quality_string(quality)
        episode["file_size_human"] = _sizeof_fmt(episode["file_size"])

        return _responds(RESULT_SUCCESS, episode)


class CMD_Episode(CMD_SickGearEpisode):
    _help = {"desc": "get detailed episode info",
             "requiredParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             "optionalParameters": {"full_path": {
                 "desc": "show the full absolute path (if valid) instead of a relative path for the episode location"},
                                   },
             "SickGearCommand": "sg.episode",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearEpisode.__init__(self, handler, args, kwargs)


class CMD_SickGearEpisodeSearch(ApiCall):
    _help = {"desc": "search for an episode. the response might take some time",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.s, args = self.check_params(args, kwargs, "season", None, True, "int", [])
        self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ search for an episode """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # retrieve the episode object and fail if we can't get one
        ep_obj = show_obj.get_episode(int(self.s), int(self.e))
        if isinstance(ep_obj, str):
            return _responds(RESULT_FAILURE, msg="Episode not found")

        # make a queue item for it and put it on the queue
        ep_queue_item = search_queue.ManualSearchQueueItem(show_obj, ep_obj)
        sickgear.search_queue_scheduler.action.add_item(ep_queue_item)

        # wait until the queue item tells us whether it worked or not
        while None is ep_queue_item.success:
            time.sleep(1)

        # return the correct json value
        if ep_queue_item.success:
            status, quality = Quality.split_composite_status(ep_obj.status)
            # TODO: split quality and status?
            return _responds(RESULT_SUCCESS, {"quality": _get_quality_string(quality)},
                             "Snatched (" + _get_quality_string(quality) + ")")

        return _responds(RESULT_FAILURE, msg='Unable to find episode')


class CMD_EpisodeSearch(CMD_SickGearEpisodeSearch):
    _help = {"desc": "search for an episode. the response might take some time",
             "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             "SickGearCommand": "sg.episode.search",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearEpisodeSearch.__init__(self, handler, args, kwargs)


class CMD_SickGearEpisodeSetStatus(ApiCall):
    _help = {"desc": "set status of an episode (or season if episode is not provided)",
             "requiredParameters": {
                 "indexer": {"desc": "indexer of a show"},
                 "indexerid": {"desc": "unique id of a show"},
                 "season": {"desc": "the season number"},
                 "status": {
                     "desc": "the status values: wanted, skipped, archived, ignored, failed, snatched, downloaded"}
             },
             "optionalParameters": {
                 "episode": {"desc": "the episode number"},
                 "force": {"desc": "should we replace existing (downloaded) episodes or not"},
                 "quality": {"desc": "set quality of episode(s), only for statuses: snatched, downloaded, archived"}}
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.s, args = self.check_params(args, kwargs, "season", None, True, "int", [])
        self.status, args = self.check_params(args, kwargs, "status", None, True, "string", [
            "wanted", "skipped", "archived", "ignored", "failed", "snatched", "downloaded"])
        # optional
        self.e, args = self.check_params(args, kwargs, "episode", None, False, "int", [])
        self.force, args = self.check_params(args, kwargs, "force", 0, False, "bool", [])
        self.quality, args = self.check_params(args, kwargs, "quality", None, False, "string", [q for q in quality_map])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ set status of an episode or a season (when no ep is provided) """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # convert the string status to a int
        for status in statusStrings.statusStrings:
            if str(statusStrings[status]).lower() == str(self.status).lower():
                self.status = status
                break
        else:  # if we dont break out of the for loop we got here.
            # the allowed values has at least one item that could not be matched against the internal status strings
            raise ApiError("The status string could not be matched to a status. Report to Devs!")

        if None is not self.quality:
            if self.status not in (SNATCHED, SNATCHED_BEST, SNATCHED_PROPER, DOWNLOADED, ARCHIVED):
                return _responds(RESULT_FAILURE, msg="Can't set status %s together with quailty: %s" %
                                                     (statusStrings[self.status], self.quality))
            self.quality = quality_map[self.quality]

        # ep_obj_list = []
        if self.e:
            ep_obj = show_obj.get_episode(self.s, self.e)
            if None is ep_obj:
                return _responds(RESULT_FAILURE, msg="Episode not found")
            ep_obj_list = [ep_obj]
        else:
            # get all episode numbers from self,season
            ep_obj_list = show_obj.get_all_episodes(season=self.s)

        def _epResult(result_code, ep, msg=""):
            return {'season': ep.season, 'episode': ep.episode, 'status': _get_status_Strings(ep.status),
                    'result': result_type_map[result_code], 'message': msg}

        ep_results = []
        failure = False
        start_backlog = False
        segments = {}

        sql_l = []
        for ep_obj in ep_obj_list:
            with ep_obj.lock:
                if self.status == WANTED:
                    # figure out what episodes are wanted so we can backlog them
                    if ep_obj.season in segments:
                        segments[ep_obj.season].append(ep_obj)
                    else:
                        segments[ep_obj.season] = [ep_obj]

                # don't let them mess up UNAIRED episodes
                if ep_obj.status == UNAIRED:
                    if None is not self.e:
                        # setting the status of a UNAIRED is only considered a failure
                        # if we directly wanted this episode, but is ignored on a season request
                        ep_results.append(
                            _epResult(RESULT_FAILURE, ep_obj, "Refusing to change status because it is UNAIRED"))
                        failure = True
                    continue

                # allow the user to force setting the status for an already downloaded episode
                if ep_obj.status in Quality.DOWNLOADED and not self.force and None is self.quality:
                    ep_results.append(_epResult(RESULT_FAILURE, ep_obj,
                                                "Refusing to change status because it is already marked as DOWNLOADED"))
                    failure = True
                    continue

                if None is not self.quality:
                    ep_obj.status = Quality.composite_status(self.status, self.quality)
                else:
                    ep_obj.status = self.status
                result = ep_obj.get_sql()
                if None is not result:
                    sql_l.append(result)

                if self.status == WANTED:
                    start_backlog = True
                ep_results.append(_epResult(RESULT_SUCCESS, ep_obj))

        if 0 < len(sql_l):
            my_db = db.DBConnection()
            my_db.mass_action(sql_l)

        extra_msg = ""
        if start_backlog:
            for season, segment in iteritems(segments):  # type: int, List[TVEpisode]
                backlog_queue_item = search_queue.BacklogQueueItem(show_obj, segment)
                sickgear.search_queue_scheduler.action.add_item(backlog_queue_item)

                self.log(f'Starting backlog for {show_obj.unique_name} season {season}'
                         f' because some episodes were set to WANTED')

            extra_msg = " Backlog started"

        if failure:
            return _responds(RESULT_FAILURE, ep_results, 'Failed to set all or some status. Check data.' + extra_msg)
        else:
            return _responds(RESULT_SUCCESS, msg='All status set successfully.' + extra_msg)


class CMD_EpisodeSetStatus(CMD_SickGearEpisodeSetStatus):
    _help = {"desc": "set status of an episode or season (when no ep is provided)",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "status": {"desc": "the status values: wanted, skipped, archived, ignored, failed"}
                                    },
             "optionalParameters": {"episode": {"desc": "the episode number"},
                                    "force": {"desc": "should we replace existing (downloaded) episodes or not"}
                                    },
             "SickGearCommand": "sg.episode.setstatus",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        kwargs['indexer'] = TVINFO_TVDB
        CMD_SickGearEpisodeSetStatus.__init__(self, handler, args, kwargs)


class CMD_SickGearSubtitleSearch(ApiCall):
    _help = {"desc": "search episode subtitles. the response might take some time",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.s, args = self.check_params(args, kwargs, "season", None, True, "int", [])
        self.e, args = self.check_params(args, kwargs, "episode", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ search episode subtitles """
        if not sickgear.USE_SUBTITLES:
            return _responds(RESULT_FAILURE, msg='Subtitle search is disabled in SickGear')

        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # retrieve the episode object and fail if we can't get one
        ep_obj = show_obj.get_episode(int(self.s), int(self.e))
        if isinstance(ep_obj, str):
            return _responds(RESULT_FAILURE, msg="Episode not found")

        # try do download subtitles for that episode
        previous_subtitles = ep_obj.subtitles

        try:
            _ = ep_obj.download_subtitles()
        except (BaseException, Exception):
            return _responds(RESULT_FAILURE, msg='Unable to find subtitles')

        # return the correct json value
        if previous_subtitles != ep_obj.subtitles:
            status = 'New subtitles downloaded: %s' % ' '.join([
                "<img src='" + sickgear.WEB_ROOT + "/images/flags/" + subliminal.language.Language(
                    x).alpha2 + ".png' alt='" + subliminal.language.Language(x).name + "'/>" for x in
                sorted(list(set(ep_obj.subtitles).difference(previous_subtitles)))])
            response = _responds(RESULT_SUCCESS, msg='New subtitles found')
        else:
            status = 'No subtitles downloaded'
            response = _responds(RESULT_FAILURE, msg='Unable to find subtitles')

        ui.notifications.message('Subtitles Search', status)

        return response


class CMD_SubtitleSearch(ApiCall):
    _help = {"desc": "search episode subtitles. the response might take some time",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"},
                                    "season": {"desc": "the season number"},
                                    "episode": {"desc": "the episode number"}
                                    },
             "SickGearCommand": "sg.episode.subtitlesearch",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        ApiCall.__init__(self, handler, args, kwargs)


class CMD_SickGearExceptions(ApiCall):
    _help = {"desc": "get scene exceptions for all or a given show",
             "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, False, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get scene exceptions for all or a given show """
        my_db = db.DBConnection(row_type="dict")

        if None is self.prodid:
            sql_result = my_db.select("SELECT s.indexer, se.show_name, se.indexer_id AS 'indexerid' "
                                      "FROM scene_exceptions AS se INNER JOIN tv_shows as s "
                                      "ON se.indexer_id == s.indexer_id")
            scene_exceptions = {}
            for cur_result in sql_result:
                indexerid = cur_result["indexerid"]
                indexer = cur_result["indexer"]
                if self.sickbeard_call:
                    if indexerid not in scene_exceptions:
                        scene_exceptions[indexerid] = []
                    scene_exceptions[indexerid].append(cur_result["show_name"])
                else:
                    if indexerid not in scene_exceptions.get(indexer, {}):
                        scene_exceptions.setdefault(indexer, {})[indexerid] = []
                    scene_exceptions.setdefault(indexer, {})[indexerid].append(cur_result["show_name"])

        else:
            show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
            if not show_obj:
                return _responds(RESULT_FAILURE, msg="Show not found")

            sql_result = my_db.select(
                "SELECT indexer, show_name, indexer_id AS 'indexerid' FROM scene_exceptions "
                "WHERE indexer = ? AND indexer_id = ?", [self.tvid, self.prodid])
            scene_exceptions = []
            for cur_result in sql_result:
                scene_exceptions.append(cur_result["show_name"])

        return _responds(RESULT_SUCCESS, scene_exceptions)


class CMD_Exceptions(CMD_SickGearExceptions):
    _help = {"desc": "get scene exceptions for all or a given show",
             "optionalParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.exceptions",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearExceptions.__init__(self, handler, args, kwargs)


class CMD_SetExceptions(ApiCall):
    _help = {"desc": "set scene exceptions for a given show",
             "requiredParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    "forseason": {"desc": "exception for season, -1 for all seasons"},
                                    },
             "optionalParameters": {"add": {"desc": "list of exceptions to add"},
                                    "remove": {"desc": "list of exceptions to remove"}},
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.forseason, args = self.check_params(args, kwargs, "forseason", None, True, "int", [])
        # optional
        self.add, args = self.check_params(args, kwargs, "add", None, False, "list", [])
        self.remove, args = self.check_params(args, kwargs, "remove", None, False, "list", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        if not self.add and not self.remove:
            return _responds(RESULT_FAILURE, 'No Exceptions provided to be add or removed.')

        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, 'Could not find any show in db from indexer: %s with id: %s' %
                             (self.tvid, self.prodid))

        my_db = db.DBConnection(row_type="dict")
        sql_result = my_db.select("SELECT show_name, season, indexer, indexer_id AS 'indexerid'"
                                  " FROM scene_exceptions"
                                  " WHERE indexer = ? AND indexer_id = ?"
                                  " AND season = ?",
                                  [self.tvid, self.prodid, self.forseason])

        cl = []
        curexep = [(s['show_name'], s['season']) for s in sql_result]
        add_list = []
        remove_list = []
        if self.remove:
            for r in self.remove:
                if (r, self.forseason) in curexep:
                    cl.append(['DELETE FROM scene_exceptions WHERE indexer = ? AND indexer_id = ? AND season = ? '
                               'AND show_name = ?', [self.tvid, self.prodid, self.forseason, r]])
                    try:
                        curexep.remove((r, self.forseason))
                    except ValueError:
                        pass
                    remove_list.append(r)

        if self.add:
            for a in self.add:
                if (a, self.forseason) not in curexep:
                    cl.append(['INSERT INTO scene_exceptions (show_name, indexer, indexer_id, season) VALUES (?,?,?,?)',
                               [a, self.tvid, self.prodid, self.forseason]])
                    curexep.append((a, self.forseason))
                    add_list.append(a)

        if cl:
            my_db.mass_action(cl)
        return _responds(RESULT_SUCCESS, data={'added': add_list, 'removed': remove_list, 'for season': self.forseason,
                                               'current': [c[0] for c in curexep], 'indexer': self.tvid,
                                               'indexerid': self.prodid},
                         msg='Exceptions changed.')


class CMD_SickGearHistory(ApiCall):
    _help = {"desc": "get the sickgear downloaded/snatched history",
             "optionalParameters": {"limit": {"desc": "limit returned results"},
                                    "type": {"desc": "only show a specific type of results"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.limit, args = self.check_params(args, kwargs, "limit", 100, False, "int", [])
        self.type, args = self.check_params(args, kwargs, "type", None, False, "string", ["downloaded", "snatched"])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the sickgear downloaded/snatched history """

        # typeCodes = []
        type_filter = []
        if "downloaded" == self.type:
            type_filter = [DOWNLOADED, ARCHIVED, FAILED]
            self.type = "Downloaded"
            typeCodes = Quality.DOWNLOADED + Quality.ARCHIVED + Quality.FAILED
        elif "snatched" == self.type:
            type_filter = SNATCHED_ANY
            self.type = "Snatched"
            typeCodes = Quality.SNATCHED_ANY
        else:
            typeCodes = Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED + Quality.FAILED

        my_db = db.DBConnection(row_type="dict")

        ulimit = min(int(self.limit), 100)
        if 0 == ulimit:
            # noinspection SqlResolve
            sql_result = my_db.select(
                "SELECT h.*, show_name, s.indexer FROM history h, tv_shows s WHERE h.hide = 0" +
                " AND h.showid=s.indexer_id" +
                ("", " AND s.indexer=%s" % TVINFO_TVDB)[self.sickbeard_call] +
                " AND action in (" + ','.join(['?'] * len(typeCodes)) + ") ORDER BY date DESC", typeCodes)
        else:
            # noinspection SqlResolve
            sql_result = my_db.select(
                "SELECT h.*, show_name, s.indexer FROM history h, tv_shows s WHERE h.hide = 0" +
                " AND h.showid=s.indexer_id" +
                ("", " AND s.indexer=%s" % TVINFO_TVDB)[self.sickbeard_call] +
                " AND action in (" + ','.join(['?'] * len(typeCodes)) + ") ORDER BY date DESC LIMIT ?",
                typeCodes + [ulimit])

        results = []
        np = NameParser(True, testing=True, indexer_lookup=False, try_scene_exceptions=False)
        for cur_result in sql_result:
            status, quality = Quality.split_composite_status(int(cur_result["action"]))
            if type_filter and status not in type_filter:
                continue
            status = _get_status_Strings(status)
            cur_result["status"] = status
            cur_result["quality"] = _get_quality_string(quality)
            cur_result["date"] = _historyDate_to_dateTimeForm(str(cur_result["date"]))
            del cur_result["action"]
            _rename_element(cur_result, "showid", "indexerid")
            cur_result["resource_path"] = os.path.dirname(cur_result["resource"])
            cur_result["resource"] = os.path.basename(cur_result["resource"])
            try:
                p_r = np.parse(cur_result["resource"], cache_result=False)
            except (BaseException, Exception):
                p_r = None
            cur_result['episode_numbers'] = (p_r and p_r.episode_numbers) or \
                                            (cur_result['episode'] and [cur_result['episode']]) or []
            # Add tvdbid for backward compatibility
            cur_result['tvdbid'] = (None, cur_result['indexerid'])[TVINFO_TVDB == cur_result['indexer']]
            results.append(cur_result)

        return _responds(RESULT_SUCCESS, results)


class CMD_History(CMD_SickGearHistory):
    _help = {"desc": "get the sickgear downloaded/snatched history",
             "optionalParameters": {"limit": {"desc": "limit returned results"},
                                    "type": {"desc": "only show a specific type of results"},
                                    },
             "SickGearCommand": "sg.history",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearHistory.__init__(self, handler, args, kwargs)


class CMD_SickGearHistoryClear(ApiCall):
    _help = {"desc": "clear the sickgear history"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ clear the sickgear history """
        my_db = db.DBConnection()
        my_db.action('UPDATE history SET hide = ? WHERE hide = 0', [1])

        return _responds(RESULT_SUCCESS, msg="History cleared")


class CMD_HistoryClear(CMD_SickGearHistoryClear):
    _help = {"desc": "clear the sickgear history",
             "SickGearCommand": "sg.history.clear"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearHistoryClear.__init__(self, handler, args, kwargs)


class CMD_SickGearHistoryTrim(ApiCall):
    _help = {"desc": "trim the sickgear history by removing entries greater than 30 days old"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ trim the sickgear history """
        my_db = db.DBConnection()
        my_db.action("UPDATE history SET hide = ? WHERE date < " + str(
            (datetime.datetime.now() - datetime.timedelta(days=30)).strftime(history.dateFormat)), [1])

        return _responds(RESULT_SUCCESS, msg="Removed history entries greater than 30 days old")


class CMD_HistoryTrim(CMD_SickGearHistoryTrim):
    _help = {"desc": "trim the sickgear history by removing entries greater than 30 days old",
             "SickGearCommand": "sg.history.trim"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearHistoryTrim.__init__(self, handler, args, kwargs)


class CMD_SickGearLogs(ApiCall):
    _help = {"desc": "get log file entries",
             "optionalParameters": {"min_level ": {
                 "desc": "the minimum level classification of log entries to return,"
                         " with each level inheriting log entries from the level above"}}
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.min_level, args = self.check_params(args, kwargs, "min_level", "error", False, "string",
                                                 ["error", "warning", "info", "debug"])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get log file entries """
        # 10 = Debug / 20 = Info / 30 = Warning / 40 = Error
        min_level = logger.reverseNames[str(self.min_level).upper()]
        max_lines = 50

        regex = r"^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$"

        final_data = []
        normal_data = []
        truncate = []
        repeated = None
        num_lines = 0

        if os.path.isfile(logger.sb_log_instance.log_file_path):
            auths = sickgear.GenericProvider.dedupe_auths(True)
            rxc_auths = re.compile('(?i)%s' % '|'.join([(re.escape(_a)) for _a in auths]))
            replacements = dict([(_a, starify(_a)) for _a in auths])
            for cur_line in logger.sb_log_instance.reverse_readline(logger.sb_log_instance.log_file_path):

                cur_line = decode_str(cur_line)
                match = re.match(regex, cur_line)

                if match:
                    level = match.group(7)
                    if level not in logger.reverseNames:
                        normal_data = []
                        continue

                    if logger.reverseNames[level] >= min_level:
                        if truncate and not normal_data and truncate[0] == match.group(8) + match.group(9):
                            truncate += [match.group(8) + match.group(9)]
                            repeated = cur_line
                            continue

                        if 1 < len(truncate):
                            data = repeated.strip() + ' (... %s repeat lines)\n' % len(truncate)
                            if not final_data:
                                final_data = [data]
                            else:
                                final_data[-1] = data

                        truncate = [match.group(8) + match.group(9)]

                        # noinspection HttpUrlsUsage
                        if 'https://' in cur_line or 'http://' in cur_line:
                            for cur_change in rxc_auths.finditer(cur_line):
                                cur_line = '%s%s%s' % (cur_line[:cur_change.start()],
                                                       replacements[cur_line[cur_change.start():cur_change.end()]],
                                                       cur_line[cur_change.end():])

                        final_data.append(cur_line)
                        if any(normal_data):
                            final_data += ['%02s) %s' % (n + 1, x) for n, x in enumerate(normal_data[::-1])] + \
                                          ['<br />']
                            num_lines += len(normal_data)
                            normal_data = []
                    else:
                        normal_data = []
                        continue

                else:
                    if not any(normal_data) and not any([cur_line.strip()]):
                        continue

                    normal_data.append(re.sub(r'\r?\n', '<br />', cur_line.replace('<', '&lt;').replace('>', '&gt;')))

                num_lines += 1

                if num_lines >= max_lines:
                    break

        return _responds(RESULT_SUCCESS, final_data)


class CMD_Logs(CMD_SickGearLogs):
    _help = {"desc": "get log file entries",
             "optionalParameters": {"min_level ": {
                 "desc": "the minimum level classification of log entries to return,"
                         " with each level inheriting log entries from the level above"}},
             "SickGearCommand": "sg.logs",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearLogs.__init__(self, handler, args, kwargs)


class CMD_SickGearPostProcess(ApiCall):
    _help = {"desc": "process completed media files to a show location",
             "optionalParameters": {"path": {"desc": "Path to process"},
                                    "force_replace": {"desc": "Force already processed dir/files"},
                                    "return_data": {"desc": "Return results for the process"},
                                    "process_method": {"desc": "Symlink, hardlink, move, or copy file(s)"},
                                    "is_priority": {"desc": "Replace file(s) even if existing at a higher quality"},
                                    "type": {"desc": "Type of media process request this is, auto or manual"},
                                    "failed": {"desc": "Mark as failed download"},
                                    "client": {"desc": "String representing the calling client"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.path, args = self.check_params(args, kwargs, "path", None, False, "string", [])
        self.force_replace, args = self.check_params(args, kwargs, "force_replace", 0, False, "bool", [])
        self.return_data, args = self.check_params(args, kwargs, "return_data", 0, False, "bool", [])
        self.process_method, args = self.check_params(args, kwargs, "process_method", False, False, "string", [
            "copy", "symlink", "hardlink", "move"])
        self.is_priority, args = self.check_params(args, kwargs, "is_priority", 0, False, "bool", [])
        self.type, args = self.check_params(args, kwargs, "type", "auto", False, "string", ["auto", "manual"])
        self.failed, args = self.check_params(args, kwargs, "failed", 0, False, "bool", [])
        self.client, args = self.check_params(args, kwargs, "client", None, False, "string", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ Starts the postprocess """
        if not self.path and not sickgear.TV_DOWNLOAD_DIR:
            return _responds(RESULT_FAILURE, msg="You need to provide a path or set TV Download Dir")

        if not self.path:
            self.path = sickgear.TV_DOWNLOAD_DIR

        if not self.type:
            self.type = 'manual'

        data = processTV.process_dir(self.path, process_method=self.process_method, force=self.force_replace,
                                     force_replace=self.is_priority, failed=self.failed, pp_type=self.type,
                                     client=self.client)

        if not self.return_data:
            data = ""

        return _responds(RESULT_SUCCESS, data=data, msg="Started postprocess for %s" % self.path)


class CMD_PostProcess(CMD_SickGearPostProcess):
    _help = {"desc": "process completed media files to a show location",
             "optionalParameters": {"path": {"desc": "Path to process"},
                                    "force_replace": {"desc": "Force already processed dir/files"},
                                    "return_data": {"desc": "Return results for the process"},
                                    "process_method": {"desc": "Symlink, hardlink, move, or copy file(s)"},
                                    "is_priority": {"desc": "Replace file(s) even if existing at a higher quality"},
                                    "type": {"desc": "Type of media process request this is, auto or manual"}
                                    },
             "SickGearCommand": "sg.postprocess",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        kwargs['failed'] = "0"
        try:
            if 'client' in kwargs:
                del kwargs['client']
        except (BaseException, Exception):
            pass
        CMD_SickGearPostProcess.__init__(self, handler, args, kwargs)


class CMD_SickGear(ApiCall):
    _help = {"desc": "get API information"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get API information """
        data = {"sb_version": sickgear.BRANCH, "api_version": Api.version, "fork": "SickGear",
                "api_commands": sorted([x for x in _functionMaper if 'listcommands' != x])}
        return _responds(RESULT_SUCCESS, data)


class CMD_SickBeard(CMD_SickGear):
    _help = {"desc": "get API information",
             "SickGearCommand": "sg", }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGear.__init__(self, handler, args, kwargs)


class CMD_SickGearAddRootDir(ApiCall):
    _help = {"desc": "add a user configured parent directory",
             "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}
                                    },
             "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"},
                                    "freespace": {"desc": "include free space of paths in response"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.location, args = self.check_params(args, kwargs, "location", None, True, "string", [])
        # optional
        self.default, args = self.check_params(args, kwargs, "default", 0, False, "bool", [])
        self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ add a user configured parent directory """

        self.location = unquote_plus(self.location)
        location_matched = 0
        index = 0

        # disallow adding/setting an invalid dir
        if not os.path.isdir(self.location):
            return _responds(RESULT_FAILURE, msg="Location is invalid")

        root_dirs = []

        if "" == sickgear.ROOT_DIRS:
            self.default = 1
        else:
            root_dirs = sickgear.ROOT_DIRS.split('|')
            index = int(sickgear.ROOT_DIRS.split('|')[0])
            root_dirs.pop(0)
            # clean up the list - replace %xx escapes by their single-character equivalent
            root_dirs = [unquote_plus(x) for x in root_dirs]
            for x in root_dirs:
                if x == self.location:
                    location_matched = 1
                    if 1 == self.default:
                        index = root_dirs.index(self.location)
                    break

        if 0 == location_matched:
            if 1 == self.default:
                root_dirs.insert(0, self.location)
            else:
                root_dirs.append(self.location)

        root_dirs_new = [unquote_plus(x) for x in root_dirs]
        root_dirs_new.insert(0, index)
        root_dirs_new = '|'.join([text_type(x) for x in root_dirs_new])

        sickgear.ROOT_DIRS = root_dirs_new
        sickgear.save_config()
        return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace),
                         msg="Root directories updated")


class CMD_SickBeardAddRootDir(CMD_SickGearAddRootDir):
    _help = {"desc": "add a user configured parent directory ",
             "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}
                                    },
             "optionalParameters": {"default": {"desc": "make the location passed the default root (parent) directory"}
                                    },
             "SickGearCommand": "sg.addrootdir",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearAddRootDir.__init__(self, handler, args, kwargs)


class CMD_SickGearCheckScheduler(ApiCall):
    _help = {"desc": "query the scheduler for event statuses, upcoming, running, and past"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ query the scheduler for event statuses, upcoming, running, and past """
        my_db = db.DBConnection()
        sql_result = my_db.select("SELECT last_backlog FROM info")

        backlogPaused = sickgear.search_queue_scheduler.action.is_backlog_paused()
        backlogRunning = sickgear.search_queue_scheduler.action.is_backlog_in_progress()
        nextBacklog = sickgear.search_backlog_scheduler.next_run().strftime(dateFormat)

        data = {"backlog_is_paused": int(backlogPaused), "backlog_is_running": int(backlogRunning),
                "last_backlog": (0 < len(sql_result) and _ordinal_to_dateForm(sql_result[0]["last_backlog"])) or '',
                "next_backlog": nextBacklog}
        return _responds(RESULT_SUCCESS, data)


class CMD_SickBeardCheckScheduler(CMD_SickGearCheckScheduler):
    _help = {"desc": "query the scheduler for event statuses, upcoming, running, and past",
             "SickGearCommand": "sg.checkscheduler"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearCheckScheduler.__init__(self, handler, args, kwargs)


class CMD_SickGearDeleteRootDir(ApiCall):
    _help = {"desc": "delete a user configured parent directory",
             "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}},
             "optionalParameters": {"freespace": {"desc": "include free space of paths in response"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.location, args = self.check_params(args, kwargs, "location", None, True, "string", [])
        # optional
        self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ delete a user configured parent directory """
        if sickgear.ROOT_DIRS == "":
            return _responds(RESULT_FAILURE, _get_root_dirs(not self.sickbeard_call and self.freespace),
                             msg="No root directories detected")

        newIndex = 0
        root_dirs_new = []
        root_dirs = sickgear.ROOT_DIRS.split('|')
        index = int(root_dirs[0])
        root_dirs.pop(0)
        # clean up the list - replace %xx escapes by their single-character equivalent
        root_dirs = [unquote_plus(x) for x in root_dirs]
        old_root_dir = root_dirs[index]
        for curRootDir in root_dirs:
            if not curRootDir == self.location:
                root_dirs_new.append(curRootDir)
            else:
                newIndex = 0

        for curIndex, curNewRootDir in enumerate(root_dirs_new):
            if curNewRootDir is old_root_dir:
                newIndex = curIndex
                break

        root_dirs_new = [unquote_plus(x) for x in root_dirs_new]
        if 0 < len(root_dirs_new):
            root_dirs_new.insert(0, newIndex)
        root_dirs_new = "|".join([text_type(x) for x in root_dirs_new])

        sickgear.ROOT_DIRS = root_dirs_new
        sickgear.save_config()
        # what if the root dir was not found?
        return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace),
                         msg="Root directory deleted")


class CMD_SickBeardDeleteRootDir(CMD_SickGearDeleteRootDir):
    _help = {"desc": "delete a user configured parent directory",
             "requiredParameters": {"location": {"desc": "the full path to root (parent) directory"}},
             "SickGearCommand": "sg.deleterootdir"
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearDeleteRootDir.__init__(self, handler, args, kwargs)


class CMD_SickGearForceSearch(ApiCall):
    _help = {'desc': 'force the specified search type to run',
             "requiredParameters": {"searchtype": {"desc": "type of search to be forced: recent, backlog, proper"}}
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.searchtype, args = self.check_params(args, kwargs, "searchtype", "recent", True, "string",
                                                  ["recent", "backlog", "proper"])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ force the specified search type to run """
        result = None
        if 'recent' == self.searchtype and not sickgear.search_queue_scheduler.action.is_recentsearch_in_progress() \
                and not sickgear.search_recent_scheduler.is_running_job:
            result = sickgear.search_recent_scheduler.force_run()
        elif 'backlog' == self.searchtype and not sickgear.search_queue_scheduler.action.is_backlog_in_progress() \
                and not sickgear.search_backlog_scheduler.is_running_job:
            sickgear.search_backlog_scheduler.force_search(force_type=FORCED_BACKLOG)
            result = True
        elif 'proper' == self.searchtype and not sickgear.search_queue_scheduler.action.is_propersearch_in_progress() \
                and not sickgear.search_propers_scheduler.is_running_job:
            result = sickgear.search_propers_scheduler.force_run()
        if result:
            return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype)
        return _responds(RESULT_FAILURE,
                         msg='Can not force the %s search because it\'s already active' % self.searchtype)


class CMD_SickBeardForceSearch(CMD_SickGearForceSearch):
    _help = {'desc': 'force the episode recent search',
             "SickGearCommand": "sg.forcesearch", }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['searchtype'] = 'recent'
        self.sickbeard_call = True
        CMD_SickGearForceSearch.__init__(self, handler, args, kwargs)


class CMD_SickGearSearchQueue(ApiCall):
    _help = {'desc': 'get a list of the sickgear search queue states'}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get a list of the sickgear search queue states """
        return _responds(RESULT_SUCCESS, sickgear.search_queue_scheduler.action.queue_length())


class CMD_SickGearGetDefaults(ApiCall):
    _help = {"desc": "get various sickgear default system values"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get various sickgear default system values """

        anyQualities, bestQualities = _mapQuality(sickgear.QUALITY_DEFAULT)

        data = {"status": statusStrings[sickgear.STATUS_DEFAULT].lower(),
                "flatten_folders": int(sickgear.FLATTEN_FOLDERS_DEFAULT), "initial": anyQualities,
                "archive": bestQualities, "future_show_paused": int(sickgear.EPISODE_VIEW_DISPLAY_PAUSED)}
        return _responds(RESULT_SUCCESS, data)


class CMD_SickBeardGetDefaults(CMD_SickGearGetDefaults):
    _help = {"desc": "get various sickgear default system values",
             "SickGearCommand": "sg.getdefaults"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearGetDefaults.__init__(self, handler, args, kwargs)


class CMD_SickGearGetMessages(ApiCall):
    _help = {"desc": "get list of ui notifications"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        messages = []
        for cur_notification in ui.notifications.get_notifications(self.handler.request.remote_ip):
            messages.append({"title": cur_notification.title,
                             "message": cur_notification.message,
                             "type": cur_notification.type})
        return _responds(RESULT_SUCCESS, messages)


class CMD_SickBeardGetMessages(CMD_SickGearGetMessages):
    _help = {"desc": "get list of ui notifications",
             "SickGearCommand": "sg.getmessages"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearGetMessages.__init__(self, handler, args, kwargs)


class CMD_SickGearGetQualities(ApiCall):
    _help = {"desc": "get globally available qualities"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        return _responds(RESULT_SUCCESS, quality_map)


class CMD_SickGearGetIndexers(ApiCall):
    _help = {"desc": "get tv info source list",
             "optionalParameters": {"searchable-only": {"desc": "only return searchable sources"}}}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.searchable_only, args = self.check_params(args, kwargs, "searchable-only", False, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        result = {}
        for i in indexer_config.tvinfo_config:
            for d, v in iteritems(indexer_config.tvinfo_config[i]):
                if self.searchable_only and (indexer_config.tvinfo_config[i].get('mapped_only') or
                                             not indexer_config.tvinfo_config[i].get('active') or
                                             indexer_config.tvinfo_config[i].get('defunct')):
                    continue
                if d in ['id', 'name', 'show_url', 'mapped_only', 'main_url'] and \
                        isinstance(v, (string_types, tuple, dict, list, integer_types, float, bool)):
                    if 'mapped_only' == d:
                        key = 'searchable'
                        val = not v and indexer_config.tvinfo_config[i].get('active') \
                            and not indexer_config.tvinfo_config[i].get('defunct')
                    else:
                        key = d
                        if 'show_url' == d:
                            val = re.sub(r'%\d{,2}d', '{INDEXER-ID}', v, flags=re.I)
                        else:
                            val = v
                    result.setdefault(i, {}).update({key: val})
        return _responds(RESULT_SUCCESS, result)


class CMD_SickGearGetIndexerIcon(ApiCall):
    _help = {"desc": "get tv info source icon",
             "requiredParameters": {"indexer": {"desc": "indexer"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().all_sources])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        # doesn't work
        i = indexer_config.tvinfo_config.get(self.tvid)
        if not i:
            self.handler.set_status(404)
            return _responds(RESULT_FAILURE, 'Icon not found')
        img = i['icon']
        image = os.path.join(sickgear.PROG_DIR, 'gui', 'slick', 'images', img)
        if not os.path.isfile(image):
            self.handler.set_status(404)
            return _responds(RESULT_FAILURE, 'Icon not found')
        if not hasattr(self.handler, '_image_list'):
            self.handler._image_list = classes.ImageUrlList()
        self.handler._image_list.add_url(image)
        return {'outputType': 'image', 'image': self.handler.get_image(image)}


class CMD_SickGearGetNetworkIcon(ApiCall):
    _help = {"desc": "get network icon",
             "requiredParameters": {"network": {"desc": "name of network"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.network, args = self.check_params(args, kwargs, "network", None, True, "string", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        image = os.path.join(sickgear.PROG_DIR, 'gui', 'slick', 'images', 'network', '%s.png' % self.network.lower())
        if not os.path.isfile(image):
            self.handler.set_status(404)
            return _responds(RESULT_FAILURE, 'Icon not found')
        if not hasattr(self.handler, '_image_list'):
            self.handler._image_list = classes.ImageUrlList()
        self.handler._image_list.add_url(image)
        return {'outputType': 'image', 'image': self.handler.get_image(image)}


class CMD_SickGearGetqualityStrings(ApiCall):
    _help = {"desc": "get human readable quality strings"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        return _responds(RESULT_SUCCESS, Quality.qualityStrings)


class CMD_SickGearGetRootDirs(ApiCall):
    _help = {"desc": "get list of user configured parent directories",
             "optionalParameters": {"freespace": {"desc": "include free space of paths in response"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.freespace, args = self.check_params(args, kwargs, "freespace", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get list of user configured parent directories """

        return _responds(RESULT_SUCCESS, _get_root_dirs(not self.sickbeard_call and self.freespace))


class CMD_SickBeardGetRootDirs(CMD_SickGearGetRootDirs):
    _help = {"desc": "get list of user configured parent directories",
             "SickGearCommand": "sg.getrootdirs"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearGetRootDirs.__init__(self, handler, args, kwargs)


class CMD_SickGearPauseBacklog(ApiCall):
    _help = {"desc": "pause the backlog search",
             "optionalParameters": {"pause": {"desc": "pause or unpause the global backlog"}}
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.pause, args = self.check_params(args, kwargs, "pause", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ pause the backlog search """
        if self.pause:
            sickgear.search_queue_scheduler.action.pause_backlog()
            return _responds(RESULT_SUCCESS, msg="Backlog paused")
        else:
            sickgear.search_queue_scheduler.action.unpause_backlog()
            return _responds(RESULT_SUCCESS, msg="Backlog unpaused")


class CMD_SickBeardPauseBacklog(CMD_SickGearPauseBacklog):
    _help = {"desc": "pause the backlog search",
             "optionalParameters": {"pause": {"desc": "pause or unpause the global backlog"}},
             "SickGearCommand": "sg.pausebacklog"
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearPauseBacklog.__init__(self, handler, args, kwargs)


class CMD_SickGearPing(ApiCall):
    _help = {"desc": "check to see if sickgear is running", }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ check to see if sickgear is running """
        self.handler.set_header('Cache-Control', "max-age=0,no-cache,no-store")
        if sickgear.started:
            return _responds(RESULT_SUCCESS, {"pid": sickgear.PID}, "Pong")
        else:
            return _responds(RESULT_SUCCESS, msg="Pong")


class CMD_SickBeardPing(CMD_SickGearPing):
    _help = {"desc": "check to see if sickgear is running",
             "SickGearCommand": "sg.ping"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearPing.__init__(self, handler, args, kwargs)


class CMD_SickGearRestart(ApiCall):
    _help = {"desc": "restart sickgear"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ restart sickgear """

        response = Scheduler.blocking_jobs()
        if response:
            msg = f'Restart aborted from API because {response.lower()}'
            logger.log(msg, logger.DEBUG)
            return _responds(RESULT_FAILURE, msg=msg)

        sickgear.restart(soft=False)
        return _responds(RESULT_SUCCESS, msg="SickGear is restarting...")


class CMD_SickBeardRestart(CMD_SickGearRestart):
    _help = {"desc": "restart sickgear",
             "SickGearCommand": "sg.restart"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearRestart.__init__(self, handler, args, kwargs)


class CMD_SickGearSearchIndexers(ApiCall):
    _help = {"desc": "search for show on a tv info source with a given string and language",
             "optionalParameters": {"name": {"desc": "name of the show you want to search for"},
                                    "indexerid": {"desc": "thetvdb.com or tvrage.com unique id of a show"},
                                    "lang": {"desc": "the 2 letter abbreviation lang id"},
                                    "indexers": {"desc": "indexer to search, use -1 to search all indexers"}
                                    }
             }

    valid_languages = {
        'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22,
        'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31,
        'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32,
        'sv': 8, 'sl': 30}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.name, args = self.check_params(args, kwargs, "name", None, False, "string", [])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", [])
        # self.lang, args = self.check_params(args, kwargs, "lang", "en", False, "string", self.valid_languages.keys())
        self.indexers, args = self.check_params(args, kwargs, "indexers", -1, False, "list",
                                                [-1] + [i for i in indexer_api.TVInfoAPI().search_sources], int)
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, False, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])

        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ search for show on a tv info source with a given string and language """
        if 1 > len(self.indexers) and -1 in self.indexers:
            raise ApiError('Mix of -1 (all Indexer) and specific Indexer not allowed')

        all_indexer = 1 == len(self.indexers) and -1 == self.indexers[0]
        lang_id = self.valid_languages['en']

        def _parse_date(dt_str):
            try:
                return dateutil.parser.parse(dt_str)
            except (BaseException, Exception):
                return ''

        if (self.name and not self.prodid) or (self.prodid and self.tvid):
            results = []
            indexertosearch = (self.indexers, [i for i in indexer_api.TVInfoAPI().sources if
                                               indexer_api.TVInfoAPI(i).config.get('active') and
                                               not indexer_api.TVInfoAPI(i).config.get('mapped_only') and
                                               not indexer_api.TVInfoAPI(i).config.get('defunct')])[all_indexer]
            search_kw = {}
            search_name = None
            if self.name:
                search_kw['name'] = decode_str(self.name)
            if self.prodid and self.tvid:
                search_kw.update({'ids': {self.tvid: self.prodid}})
                if self.tvid in indexertosearch:
                    indexertosearch = [self.tvid] + [i for i in indexertosearch if i != self.tvid]

            for i in indexertosearch:
                tvinfo_config = sickgear.TVInfoAPI(i).api_params.copy()
                tvinfo_config['language'] = 'en'
                tvinfo_config['custom_ui'] = classes.AllShowInfosNoFilterListUI
                t = sickgear.TVInfoAPI(i).setup(**tvinfo_config)

                try:
                    apiData = t.search_show(**search_kw)
                except (BaseException, Exception) as e:
                    continue

                if not apiData:
                    continue

                for curSeries in (apiData or []):
                    if (int(curSeries['id']) == self.prodid and i == self.tvid) or \
                            any({self.tvid: self.prodid} == {k: v} for k, v in iteritems(curSeries['ids'])):
                        if not self.name and curSeries['ids'].get(self.tvid) == int(curSeries['id']):
                            search_kw['name'] = curSeries['seriesname']
                            search_name = curSeries['seriesname']
                        new_ids = {k: v for k, v in iteritems(curSeries['ids']) if v and k != self.tvid}
                        if new_ids:
                            if 'ids' not in search_kw:
                                search_kw['ids'] = {}
                            search_kw['ids'].update(new_ids)

                    s = {"indexerid": int(curSeries['id']),
                         "name": helpers.normalise_chars(curSeries['seriesname']),
                         "first_aired": isinstance(curSeries['firstaired'], string_types)
                         and SGDatetime.sbfdate(_parse_date(curSeries['firstaired']), d_preset=dateFormat) or '',
                         "indexer": i,
                         "aliases": curSeries.get('aliases') or [],
                         "network": curSeries.get('network') or '',
                         "overview": helpers.normalise_chars(curSeries.get('overview') or ''),
                         "language": curSeries.get('language') or '',
                         "genres": (curSeries.get('genres', '') or curSeries.get('genre', '') or '').replace('|', ', '),
                         "genre_list": curSeries.get('genre_list') or [],
                         "poster": curSeries.get('poster') or '',
                         "relevance": (curSeries['ids'].get(self.tvid) == int(curSeries['id']) and 100)
                         or AddShows.get_uw_ratio(self.name or search_name or '', curSeries['seriesname'],
                                                  curSeries.get('aliases') or [])}
                    if TVINFO_TVDB == i:
                        s["tvdbid"] = int(curSeries['id'])
                    else:
                        s["tvdbid"] = ''
                    results.append(s)

            if not results:
                return _responds(RESULT_FAILURE, msg="Did not get result from %s" %
                                                     ', '.join([sickgear.TVInfoAPI(i).name for i in indexertosearch]))

            results = sorted(results, key=lambda x: x['relevance'], reverse=True)

            return _responds(RESULT_SUCCESS, {"results": results, "langid": lang_id, "lang": 'en'})

        return _responds(RESULT_FAILURE, msg="Either indexerid or name is required")


class CMD_SickBeardSearchIndexers(CMD_SickGearSearchIndexers):
    _help = {"desc": "search for show on the tvdb with a given string and language",
             "optionalParameters": {"name": {"desc": "name of the show you want to search for"},
                                    "indexerid": {"desc": "thetvdb.com unique id of a show"},
                                    "lang": {"desc": "the 2 letter abbreviation lang id"},
                                    },
             "SickGearCommand": "sg.searchtv",
             }

    def __init__(self, handler, args, kwargs):
        kwargs['indexers'] = TVINFO_TVDB
        kwargs['indexer'] = TVINFO_TVDB
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearSearchIndexers.__init__(self, handler, args, kwargs)


class CMD_SickGearSetDefaults(ApiCall):
    _help = {"desc": "set various sickgear default system values",
             "optionalParameters": {"initial": {"desc": "initial quality to use when adding shows"},
                                    "archive": {"desc": "archive quality to use when adding shows"},
                                    "flatten_folders": {"desc": "flatten show subfolders when adding shows"},
                                    "status": {"desc": "status to change episodes to with missing media"},
                                    "future_show_paused": {"desc": "show/hide paused shows on the daily schedule page"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map])
        self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map])
        self.future_show_paused, args = self.check_params(args, kwargs, "future_show_paused", None, False, "bool", [])
        self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders", None, False, "bool", [])
        self.status, args = self.check_params(args, kwargs, "status", None, False, "string",
                                              ["wanted", "skipped", "archived", "ignored"])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ set various sickgear default system values """

        iqualityID = []
        aqualityID = []

        if self.initial:
            for quality in self.initial:
                iqualityID.append(quality_map[quality])
        if self.archive:
            for quality in self.archive:
                aqualityID.append(quality_map[quality])

        if iqualityID or aqualityID:
            sickgear.QUALITY_DEFAULT = Quality.combine_qualities(iqualityID, aqualityID)

        if self.status:
            # convert the string status to a int
            for status in statusStrings.statusStrings:
                if statusStrings[status].lower() == str(self.status).lower():
                    self.status = status
                    break
            # this should be obsolete because of the above
            if self.status not in statusStrings.statusStrings:
                raise ApiError("Invalid Status")
            # only allow the status options we want
            if int(self.status) not in (3, 5, 6, 7):
                raise ApiError("Status Prohibited")
            sickgear.STATUS_DEFAULT = self.status

        if None is not self.flatten_folders:
            sickgear.FLATTEN_FOLDERS_DEFAULT = int(self.flatten_folders)

        if None is not self.future_show_paused:
            sickgear.EPISODE_VIEW_DISPLAY_PAUSED = int(self.future_show_paused)

        sickgear.save_config()

        return _responds(RESULT_SUCCESS, msg="Saved defaults")


class CMD_SickBeardSetDefaults(CMD_SickGearSetDefaults):
    _help = {"desc": "set various sickgear default system values",
             "optionalParameters": {"initial": {"desc": "initial quality to use when adding shows"},
                                    "archive": {"desc": "archive quality to use when adding shows"},
                                    "flatten_folders": {"desc": "flatten show subfolders when adding shows"},
                                    "status": {"desc": "status to change episodes to with missing media"}
             },
             "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 for a show",
             "requiredParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    },
             "optionalParameters": {"forSeason": {"desc": "season number of a show"},
                                    "forEpisode": {"desc": "episode number of a show"},
                                    "forAbsolute": {"desc": "absolute episode number of a show"},
                                    "sceneSeason": {"desc": "scene season number of a show to set"},
                                    "sceneEpisode": {"desc": "scene episode number of a show to set"},
                                    "sceneAbsolute": {"desc": "scene absolute episode number of a show to set"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.tvid, args = self.check_params(
            args, kwargs, "indexer", None, True, "int", [i for i in indexer_api.TVInfoAPI().sources])
        # optional
        self.forSeason, args = self.check_params(args, kwargs, "forSeason", None, False, "int", [])
        self.forEpisode, args = self.check_params(args, kwargs, "forEpisode", None, False, "int", [])
        self.forAbsolute, args = self.check_params(args, kwargs, "forAbsolute", None, False, "int", [])
        self.sceneSeason, args = self.check_params(args, kwargs, "sceneSeason", None, False, "int", [])
        self.sceneEpisode, args = self.check_params(args, kwargs, "sceneEpisode", None, False, "int", [])
        self.sceneAbsolute, args = self.check_params(args, kwargs, "sceneAbsolute", None, False, "int", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ saving scene numbers """

        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Can't find show")
        if not show_obj.is_scene:
            return _responds(RESULT_FAILURE, msg="Show scene numbering disabled")

        result = set_scene_numbering_helper(self.tvid, self.prodid, self.forSeason, self.forEpisode,
                                            self.forAbsolute, self.sceneSeason, self.sceneEpisode, self.sceneAbsolute)

        if not result['success']:
            return _responds(RESULT_FAILURE, result)

        return _responds(RESULT_SUCCESS, result)


class CMD_SickGearActivateSceneNumber(ApiCall):
    _help = {"desc": "de-/activate scene numbers",
             "requiredParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    "activate": {"desc": "de-/activate scene numbering"}},
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.activate, args = self.check_params(args, kwargs, "activate", None, True, "bool", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ de-/activate scene numbers """

        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Can't find show")

        show_obj.scene = int(self.activate)
        show_obj.save_to_db()

        return _responds(RESULT_SUCCESS, data={'indexer': self.tvid, 'indexerid': self.prodid,
                                               'show_name': show_obj.name, 'scenenumbering': show_obj.is_scene},
                         msg="Scene Numbering %sactivated" % ('de', '')[self.activate])


class CMD_SickGearShutdown(ApiCall):
    _help = {"desc": "shutdown sickgear"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ shutdown sickgear """

        response = Scheduler.blocking_jobs()
        if response:
            msg = f'Shutdown aborted from API because {response.lower()}'
            logger.log(msg, logger.DEBUG)
            return _responds(RESULT_FAILURE, msg=msg)

        sickgear.events.put(sickgear.events.SystemEvent.SHUTDOWN)
        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": "get ignore word list (uses global list if both indexerid and indexer params are not set)",
             "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, False, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get ignore word list """
        if self.tvid and self.prodid:
            my_db = db.DBConnection()
            sql_result = my_db.select(
                'SELECT show_name, rls_ignore_words, rls_global_exclude_ignore'
                ' FROM tv_shows'
                ' WHERE indexer = ? AND indexer_id = ?',
                [self.tvid, self.prodid])
            if sql_result:
                ignore_words = sql_result[0]['rls_ignore_words']
                return_data = {'type': 'show', 'indexer': self.tvid, 'indexerid': self.prodid,
                               'show name': sql_result[0]['show_name'],
                               'global exclude ignore':
                                   helpers.split_word_str(sql_result[0]['rls_global_exclude_ignore'])[0]}
                return_type = '%s:' % sql_result[0]['show_name']
            else:
                return _responds(RESULT_FAILURE, msg='Show not found.')
        elif (None is self.tvid) != (None is self.prodid):
            return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.')
        else:
            ignore_words = helpers.generate_word_str(sickgear.IGNORE_WORDS, sickgear.IGNORE_WORDS_REGEX)
            return_data = {'type': 'global'}
            return_type = 'Global'

        return_data['use regex'] = ignore_words.startswith('regex:')
        return_data['ignore words'] = [w.strip() for w in ignore_words.replace('regex:', '').split(',') if w.strip()]
        return _responds(RESULT_SUCCESS, data=return_data, msg="%s ignore word list" % return_type)


class ApiSetWords(ApiCall):

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, False, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.add, args = self.check_params(args, kwargs, "add", None, False, "list", [])
        self.remove, args = self.check_params(args, kwargs, "remove", None, False, "list", [])
        self.add_exclude, args = self.check_params(args, kwargs, "add_exclude", None, False, "list", [])
        self.remove_exclude, args = self.check_params(args, kwargs, "remove_exclude", None, False, "list", [])
        self.regex, args = self.check_params(args, kwargs, "regex", None, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def _create_words(self, words, exclude_list=None):
        exclude_list = exclude_list or []
        word_list = {w.strip() for w in words.replace('regex:', '').split(',') if w.strip()}

        use_regex = self.regex if None is not self.regex else words.startswith('regex:')

        for a in self.add or []:
            if a not in exclude_list:
                word_list.add(a.strip())
        for r in self.remove or []:
            try:
                word_list.remove(r)
            except KeyError:
                pass

        return use_regex, word_list, \
            ('', '%s%s' % (('', 'regex:')[use_regex],
                           ', '.join([w.strip() for w in word_list if w.strip()])))[0 < len(word_list)]


class CMD_SickGearSetIgnoreWords(ApiSetWords):
    _help = {"desc": "set ignore word list (uses global list if both indexerid and indexer params are not set)",
             "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    "add": {"desc": "add words to list"},
                                    "remove": {"desc": "remove words from list"},
                                    "add_exclude": {"desc": "add global exclude words"},
                                    "remove_exclude": {"desc": "remove global exclude words"},
                                    "regex": {"desc": "interpret ALL (including existing) ignore words as regex"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        super(CMD_SickGearSetIgnoreWords, self).__init__(handler, args, kwargs)

    def run(self):
        """ set ignore word list """
        if (not self.add and not self.remove and not self.add_exclude and not self.remove_exclude) or \
                ((self.add_exclude or self.remove_exclude) and not (self.tvid and self.prodid)):
            return _responds(RESULT_FAILURE, msg=('No indexer, indexerid provided',
                                                  'No words to add/remove provided')[None is not self.tvid and
                                                                                     None is not self.prodid])

        use_regex = None
        return_type = ''
        ignore_list = set()

        if self.tvid and self.prodid:
            show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
            if not show_obj:
                return _responds(RESULT_FAILURE, msg="Show not found")

            return_data = {'type': 'show', 'indexer': self.tvid, 'indexerid': self.prodid,
                           'show name': show_obj.name}

            if any([self.add, self.remove, self.add_exclude, self.remove_exclude]):
                my_db = db.DBConnection()
                sql_results = my_db.select(
                    'SELECT show_name, rls_ignore_words, rls_global_exclude_ignore'
                    ' FROM tv_shows'
                    ' WHERE indexer = ? AND indexer_id = ?', [self.tvid, self.prodid])
                if sql_results:
                    ignore_words = sql_results[0]['rls_ignore_words']
                    ignore_list, use_regex = helpers.split_word_str(ignore_words)
                    exclude_ignore = helpers.split_word_str(sql_results[0]['rls_global_exclude_ignore'])[0]
                    exclude_ignore = {i for i in exclude_ignore if i in sickgear.IGNORE_WORDS}
                    return_type = '%s:' % sql_results[0]['show_name']

                    if self.add or self.remove:
                        use_regex, ignore_list, new_ignore_words = \
                            self._create_words(ignore_words, sickgear.IGNORE_WORDS)
                        my_db.action('UPDATE tv_shows'
                                     ' SET rls_ignore_words = ?'
                                     ' WHERE indexer = ? AND indexer_id = ?',
                                     [new_ignore_words, self.tvid, self.prodid])
                        show_obj.rls_ignore_words, show_obj.rls_ignore_words_regex = \
                            helpers.split_word_str(new_ignore_words)

                    if self.add_exclude or self.remove_exclude:
                        for a in self.add_exclude or []:
                            if a in sickgear.IGNORE_WORDS:
                                exclude_ignore.add(a)
                        for r in self.remove_exclude or []:
                            try:
                                exclude_ignore.remove(r)
                            except KeyError:
                                pass

                        my_db.action('UPDATE tv_shows SET rls_global_exclude_ignore = ?'
                                     ' WHERE indexer = ? AND indexer_id = ?',
                                     [helpers.generate_word_str(exclude_ignore), self.tvid, self.prodid])
                        show_obj.rls_global_exclude_ignore = copy.copy(exclude_ignore)

                    return_data['global exclude ignore'] = exclude_ignore
        elif (None is self.tvid) != (None is self.prodid):
            return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.')
        else:
            ignore_words = helpers.generate_word_str(sickgear.IGNORE_WORDS, sickgear.IGNORE_WORDS_REGEX)
            use_regex, ignore_list, new_ignore_words = self._create_words(ignore_words)
            sickgear.IGNORE_WORDS, sickgear.IGNORE_WORDS_REGEX = helpers.split_word_str(new_ignore_words)
            sickgear.save_config()
            return_data = {'type': 'global'}
            return_type = 'Global'

        if None is not use_regex:
            return_data['use regex'] = use_regex
        elif None is not self.regex:
            return_data['use regex'] = self.regex
        return_data['ignore words'] = ignore_list
        clean_ignore_require_words()
        return _responds(RESULT_SUCCESS, data=return_data, msg="%s set ignore word list" % return_type)


class CMD_SickGearListRequireWords(ApiCall):
    _help = {"desc": "get require word list (uses global list if both indexerid and indexer params are not set)",
             "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, False, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, False, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)
        self.old_call = False

    def run(self):
        """ get require word list """
        if self.tvid and self.prodid:
            my_db = db.DBConnection()
            sql_result = my_db.select(
                'SELECT show_name, rls_require_words, rls_global_exclude_require'
                ' FROM tv_shows'
                ' WHERE indexer = ? AND indexer_id = ?',
                [self.tvid, self.prodid])
            if sql_result:
                require_words = sql_result[0]['rls_require_words']
                return_data = {'type': 'show', 'indexer': self.tvid, 'indexerid': self.prodid,
                               'show name': sql_result[0]['show_name'],
                               'global exclude require':
                                   helpers.split_word_str(sql_result[0]['rls_global_exclude_require'])[0]}
                return_type = '%s:' % sql_result[0]['show_name']
            else:
                return _responds(RESULT_FAILURE, msg='Show not found.')
        elif (None is self.tvid) != (None is self.prodid):
            return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.')
        else:
            require_words = helpers.generate_word_str(sickgear.REQUIRE_WORDS, sickgear.REQUIRE_WORDS_REGEX)
            return_data = {'type': 'global'}
            return_type = 'Global'

        return_data['use regex'] = require_words.startswith('regex:')
        return_data['require%s words' % ('', 'd')[self.old_call]] = [w.strip() for w in
                                                                     require_words.replace('regex:', '').split(',')
                                                                     if w.strip()]
        return _responds(RESULT_SUCCESS, data=return_data, msg="%s require word list" % return_type)


class CMD_SickGearListRequireWords_old(CMD_SickGearListRequireWords):
    def __init__(self, handler, args, kwargs):
        CMD_SickGearListRequireWords.__init__(self, handler, args, kwargs)
        self.old_call = True


class CMD_SickGearSetRequireWords(ApiSetWords):
    _help = {"desc": "set require word list (uses global list if both indexerid and indexer params are not set)",
             "optionalParameters": {"indexerid": {"desc": "unique id of a show"},
                                    "indexer": {"desc": "indexer of a show"},
                                    "add": {"desc": "add words to list"},
                                    "remove": {"desc": "remove words from list"},
                                    "add_exclude": {"desc": "add global exclude words"},
                                    "remove_exclude": {"desc": "remove global exclude words"},
                                    "regex": {"desc": "interpret ALL (including existing) ignore words as regex"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        super(CMD_SickGearSetRequireWords, self).__init__(handler, args, kwargs)
        self.old_call = False

    def run(self):
        """ set require words """
        if (not self.add and not self.remove and not self.add_exclude and not self.remove_exclude) or \
                ((self.add_exclude or self.remove_exclude) and not (self.tvid and self.prodid)):
            return _responds(RESULT_FAILURE, msg=('No indexer, indexerid provided',
                                                  'No words to add/remove provided')[None is not self.tvid and
                                                                                     None is not self.prodid])

        use_regex = None
        return_type = ''
        require_list = set()

        if self.tvid and self.prodid:
            show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
            if not show_obj:
                return _responds(RESULT_FAILURE, msg="Show not found")

            return_data = {'type': 'show', 'indexer': self.tvid, 'indexerid': self.prodid,
                           'show name': show_obj.name}

            if any([self.add, self.remove, self.add_exclude, self.remove_exclude]):
                my_db = db.DBConnection()
                sql_result = my_db.select(
                    'SELECT show_name, rls_require_words, rls_global_exclude_require'
                    ' FROM tv_shows'
                    ' WHERE indexer = ? AND indexer_id = ?', [self.tvid, self.prodid])
                if sql_result:
                    require_words = sql_result[0]['rls_require_words']
                    require_list, use_regex = helpers.split_word_str(require_words)
                    exclude_require = helpers.split_word_str(sql_result[0]['rls_global_exclude_require'])[0]
                    exclude_require = {r for r in exclude_require if r in sickgear.REQUIRE_WORDS}
                    return_type = '%s:' % sql_result[0]['show_name']

                    if self.add or self.remove:
                        use_regex, require_list, new_require_words = \
                            self._create_words(require_words, sickgear.REQUIRE_WORDS)
                        my_db.action('UPDATE tv_shows'
                                     ' SET rls_require_words = ?'
                                     ' WHERE indexer = ? AND indexer_id = ?',
                                     [new_require_words, self.tvid, self.prodid])
                        show_obj.rls_require_words, show_obj.rls_require_words_regex = \
                            helpers.split_word_str(new_require_words)

                    if self.add_exclude or self.remove_exclude:
                        for a in self.add_exclude or []:
                            if a in sickgear.REQUIRE_WORDS:
                                exclude_require.add(a)
                        for r in self.remove_exclude or []:
                            try:
                                exclude_require.remove(r)
                            except KeyError:
                                pass
                        my_db.action(
                            'UPDATE tv_shows SET rls_global_exclude_require = ?'
                            ' WHERE indexer = ? AND indexer_id = ?',
                            [helpers.generate_word_str(exclude_require), self.tvid, self.prodid])
                        show_obj.rls_global_exclude_require = copy.copy(exclude_require)

                    return_data['global exclude require'] = exclude_require
        elif (None is self.tvid) != (None is self.prodid):
            return _responds(RESULT_FAILURE, msg='You must supply indexer + indexerid.')
        else:
            require_words = helpers.generate_word_str(sickgear.REQUIRE_WORDS, sickgear.REQUIRE_WORDS_REGEX)
            use_regex, require_list, new_require_words = self._create_words(require_words)

            sickgear.REQUIRE_WORDS, sickgear.REQUIRE_WORDS_REGEX = helpers.split_word_str(new_require_words)
            sickgear.save_config()
            return_data = {'type': 'global'}
            return_type = 'Global'

        if None is not use_regex:
            return_data['use regex'] = use_regex
        elif None is not self.regex:
            return_data['use regex'] = self.regex
        return_data['require%s words' % ('', 'd')[self.old_call]] = require_list
        clean_ignore_require_words()
        return _responds(RESULT_SUCCESS, data=return_data,
                         msg="%s set %s" % (return_type, ('require word list', 'requried words')[self.old_call]))


class CMD_SickGearSetRequireWords_old(CMD_SickGearSetRequireWords):
    def __init__(self, handler, args, kwargs):
        CMD_SickGearSetRequireWords.__init__(self, handler, args, kwargs)
        self.old_call = True


class CMD_SickGearUpdateWatchedState(ApiCall):
    _help = {"desc": "Update db with details of media file(s) that are watched or unwatched",
             "requiredParameters": {
                 "payloadjson": {
                     "desc": "a dict of dicts transmitted as JSON via POST request"},
             }}

    def __init__(self, handler, args, kwargs):
        # required
        self.payloadjson, args = self.check_params(args, kwargs, "payloadjson", None, True, "dict", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ Update db with details of media file that is watched or unwatched """
        payload = self.payloadjson.copy()

        webserve.MainHandler.update_watched_state(payload, as_json=False)

        if not payload:
            return _responds(RESULT_FAILURE, msg='Request made to SickGear with invalid payload')

        return _responds(RESULT_SUCCESS, payload)


class CMD_SickGearShow(ApiCall):
    _help = {"desc": "get show information",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             "optionalParameters": {"overview": {"desc": "include overview"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        self.overview, args = self.check_params(args, kwargs, "overview", False, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get show information """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        showDict = {"season_list": CMD_SickGearShowSeasonList(self.handler, (),
                                                              {'indexer': self.tvid, 'indexerid': self.prodid}
                                                              ).run()['data'],
                    "cache": CMD_SickGearShowCache(self.handler, (), {'indexer': self.tvid,
                                                                      'indexerid': self.prodid}).run()['data']}

        genreList = []
        if show_obj.genre:
            genreListTmp = show_obj.genre.split("|")
            for genre in genreListTmp:
                if genre:
                    genreList.append(genre)
        showDict["genre"] = genreList
        showDict["quality"] = _get_quality_string(show_obj.quality)

        anyQualities, bestQualities = _mapQuality(show_obj.quality)
        showDict["quality_details"] = {"initial": anyQualities, "archive": bestQualities}

        try:
            showDict["location"] = show_obj.location
        except exceptions_helper.ShowDirNotFoundException:
            showDict["location"] = ""

        showDict["language"] = show_obj.lang
        showDict["show_name"] = show_obj.name
        showDict["paused"] = show_obj.paused
        showDict["subtitles"] = show_obj.subtitles
        showDict["air_by_date"] = show_obj.air_by_date
        showDict["flatten_folders"] = show_obj.flatten_folders
        showDict["sports"] = show_obj.sports
        showDict["anime"] = show_obj.anime
        # clean up tvdb horrible airs field
        showDict["airs"] = str(show_obj.airs).replace('am', ' AM').replace('pm', ' PM').replace('  ', ' ')
        showDict["indexerid"] = self.prodid
        showDict["tvrage_id"] = show_obj.ids.get(TVINFO_TVRAGE, {'id': 0})['id']
        showDict['ids'] = {k: v.get('id') for k, v in iteritems(show_obj.ids)}
        showDict["tvrage_name"] = show_obj.name
        showDict["network"] = show_obj.network
        if not showDict["network"]:
            showDict["network"] = ""
        showDict["status"] = show_obj.status
        showDict["scenenumbering"] = show_obj.is_scene
        showDict["upgrade_once"] = show_obj.upgrade_once
        showDict["ignorewords"] = helpers.generate_word_str(show_obj.rls_ignore_words, show_obj.rls_ignore_words_regex)
        showDict["global_exclude_ignore"] = helpers.generate_word_str(show_obj.rls_global_exclude_ignore)
        showDict["requirewords"] = helpers.generate_word_str(show_obj.rls_require_words, show_obj.rls_require_words_regex)
        showDict["global_exclude_require"] = helpers.generate_word_str(show_obj.rls_global_exclude_require)
        if self.overview:
            showDict["overview"] = show_obj.overview
        showDict["prune"] = show_obj.prune
        showDict["tag"] = show_obj.tag
        showDict["imdb_id"] = show_obj.imdbid
        showDict["classification"] = show_obj.classification
        showDict["runtime"] = show_obj.runtime
        showDict["startyear"] = show_obj.startyear
        showDict["indexer"] = show_obj.tvid
        timezone, showDict['timezone'] = network_timezones.get_network_timezone(showDict['network'], return_name=True)

        if show_obj.next_episode():
            dtEpisodeAirs = SGDatetime.convert_to_setting(
                network_timezones.parse_date_time(show_obj.nextaired, showDict['airs'], timezone))
            showDict['airs'] = SGDatetime.sbftime(dtEpisodeAirs, t_preset=timeFormat).lstrip('0').replace(' 0', ' ')
            showDict['next_ep_airdate'] = SGDatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat)
        else:
            showDict['next_ep_airdate'] = ''

        return _responds(RESULT_SUCCESS, showDict)


class CMD_Show(CMD_SickGearShow):
    _help = {"desc": "get thetvdb.com show information",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "optionalParameters": {"overview": {"desc": "include overview"},
                                    },
             "SickGearCommand": "sg.show",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShow.__init__(self, handler, args, kwargs)


class CMD_SickGearShowAddExisting(ApiCall):
    _help = {"desc": "add a show to SickGear from an existing folder",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "production id of a show"},
                                    "location": {"desc": "full path to the existing folder for the show"}
                                    },
             "optionalParameters": {"initial": {"desc": "initial quality for the show"},
                                    "archive": {"desc": "archive quality for the show"},
                                    "upgrade_once": {"desc": "upgrade only once"},
                                    "pause": {"desc": "pause show search tasks to allow edits"},
                                    "subtitles": {"desc": "allow search episode subtitle"},
                                    "flatten_folders": {"desc": "flatten subfolders for the show"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().search_sources])
        self.location, args = self.check_params(args, kwargs, "location", None, True, "string", [])
        # optional
        self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map])
        self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map])
        self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", [])

        self.pause, args = self.check_params(args, kwargs, "pause", int(sickgear.PAUSE_DEFAULT), False, "int", [])
        self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders",
                                                       str(sickgear.FLATTEN_FOLDERS_DEFAULT), False, "bool", [])
        self.subtitles, args = self.check_params(args, kwargs, "subtitles", int(sickgear.USE_SUBTITLES), False, "int",
                                                 [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ add a show to SickGear from an existing folder """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if show_obj:
            return _responds(RESULT_FAILURE, msg="An existing indexerid already exists in the database")

        if not os.path.isdir(self.location):
            return _responds(RESULT_FAILURE, msg='Not a valid location')

        lINDEXER_API_PARMS = sickgear.TVInfoAPI(self.tvid).api_params.copy()
        lINDEXER_API_PARMS['language'] = 'en'
        lINDEXER_API_PARMS['custom_ui'] = classes.AllShowInfosNoFilterListUI
        lINDEXER_API_PARMS['actors'] = False

        t = sickgear.TVInfoAPI(self.tvid).setup(**lINDEXER_API_PARMS)

        try:
            myShow = t.get_show(self.prodid, load_episodes=False)
        except BaseTVinfoError as e:
            self.log(f'Unable to find show with id {self.tvid}', logger.WARNING)
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        indexerName = None
        if None is myShow or getattr(t, 'show_not_found', False) or not myShow.data['seriesname']:
            self.log(
                "Found show with tvid %s prodid %s, however it contained no show name" % (self.tvid, self.prodid),
                logger.DEBUG)
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        else:
            indexerName = myShow.data['seriesname']

        if not indexerName:
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        # use default quality as a failsafe
        newQuality = int(sickgear.QUALITY_DEFAULT)
        iqualityID = []
        aqualityID = []

        if self.initial:
            for quality in self.initial:
                iqualityID.append(quality_map[quality])
        if self.archive:
            for quality in self.archive:
                aqualityID.append(quality_map[quality])

        if iqualityID or aqualityID:
            newQuality = Quality.combine_qualities(iqualityID, aqualityID)

        sickgear.show_queue_scheduler.action.add_show(
            int(self.tvid), int(self.prodid), self.location,
            quality=newQuality, upgrade_once=self.upgradeonce,
            paused=self.pause, default_status=SKIPPED, flatten_folders=int(self.flatten_folders)
        )

        return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added")


class CMD_ShowAddExisting(CMD_SickGearShowAddExisting):
    _help = {"desc": "add a show to SickGear from an existing folder",
             "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id"},
                                    "location": {"desc": "full path to the existing folder for the show"}
                                    },
             "optionalParameters": {"initial": {"desc": "initial quality for the show"},
                                    "archive": {"desc": "archive quality for the show"},
                                    "flatten_folders": {"desc": "flatten subfolders for the show"},
                                    "subtitles": {"desc": "allow search episode subtitle"}
                                    },
             "SickGearCommand": "sg.show.addexisting",
             }

    def __init__(self, handler, args, kwargs):
        kwargs['indexer'] = TVINFO_TVDB
        # required
        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)


class CMD_SickGearShowAddNew(ApiCall):
    _help = {"desc": "add a new show to sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of show"},
                                    "indexerid": {"desc": "id of show"},
                                    },
             "optionalParameters": {"initial": {"desc": "initial quality for the show"},
                                    "location": {"desc": "base path for where the show folder is to be created"},
                                    "archive": {"desc": "archive quality for the show"},
                                    "upgrade_once": {"desc": "upgrade only once"},
                                    "flatten_folders": {"desc": "flatten subfolders for the show"},
                                    "status": {"desc": "status of missing episodes"},
                                    "subtitles": {"desc": "allow search episode subtitle"},
                                    "anime": {"desc": "set show to anime"},
                                    "scene": {"desc": "show searches episodes by scene numbering"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().search_sources])
        # optional
        self.location, args = self.check_params(args, kwargs, "location", None, False, "string", [])
        self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map])
        self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map])
        self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", [])
        self.pause, args = self.check_params(args, kwargs, "pause", int(sickgear.PAUSE_DEFAULT), False, "int", [])
        self.status, args = self.check_params(args, kwargs, "status", None, False, "string",
                                              ["wanted", "skipped", "archived", "ignored"])
        self.scene, args = self.check_params(args, kwargs, "scene", int(sickgear.SCENE_DEFAULT), False, "int", [])
        self.subtitles, args = self.check_params(
            args, kwargs, "subtitles", int(sickgear.USE_SUBTITLES), False, "int", [])
        self.flatten_folders, args = self.check_params(args, kwargs, "flatten_folders",
                                                       str(sickgear.FLATTEN_FOLDERS_DEFAULT), False, "bool", [])
        self.anime, args = self.check_params(args, kwargs, "anime", int(sickgear.ANIME_DEFAULT), False, "int", [])
        self.lang = 'en'

        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ add a show to SickGear from an existing folder """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if show_obj:
            return _responds(RESULT_FAILURE, msg="An existing indexerid already exists in database")

        if not self.location:
            if "" != sickgear.ROOT_DIRS:
                root_dirs = sickgear.ROOT_DIRS.split('|')
                root_dirs.pop(0)
                default_index = int(sickgear.ROOT_DIRS.split('|')[0])
                self.location = root_dirs[default_index]
            else:
                return _responds(RESULT_FAILURE, msg="Root directory is not set, please provide a location")

        if not os.path.isdir(self.location):
            return _responds(RESULT_FAILURE, msg="'" + self.location + "' is not a valid location")

        # use default quality as a failsafe
        newQuality = int(sickgear.QUALITY_DEFAULT)
        iqualityID = []
        aqualityID = []

        if self.initial:
            for quality in self.initial:
                iqualityID.append(quality_map[quality])
        if self.archive:
            for quality in self.archive:
                aqualityID.append(quality_map[quality])

        if iqualityID or aqualityID:
            newQuality = Quality.combine_qualities(iqualityID, aqualityID)

        # use default status as a failsafe
        newStatus = sickgear.STATUS_DEFAULT
        if self.status:
            # convert the string status to a int
            for status in statusStrings.statusStrings:
                if statusStrings[status].lower() == str(self.status).lower():
                    self.status = status
                    break
            # TODO: check if obsolete
            if self.status not in statusStrings.statusStrings:
                raise ApiError("Invalid Status")
            # only allow the status options we want
            if int(self.status) not in (3, 5, 6, 7):
                return _responds(RESULT_FAILURE, msg="Status prohibited")
            newStatus = self.status

        lINDEXER_API_PARMS = sickgear.TVInfoAPI(self.tvid).api_params.copy()
        lINDEXER_API_PARMS['language'] = 'en'
        lINDEXER_API_PARMS['custom_ui'] = classes.AllShowInfosNoFilterListUI
        lINDEXER_API_PARMS['actors'] = False

        t = sickgear.TVInfoAPI(self.tvid).setup(**lINDEXER_API_PARMS)

        try:
            myShow = t.get_show(self.prodid, load_episodes=False)
        except BaseTVinfoError as e:
            self.log(f'Unable to find show with id {self.tvid}', logger.WARNING)
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        indexerName = None
        if None is myShow or getattr(t, 'show_not_found', False) or not myShow.data['seriesname']:
            self.log(
                "Found show with tvid %s prodid %s, however it contained no show name" % (self.tvid, self.prodid),
                logger.DEBUG)
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        else:
            indexerName = myShow.data['seriesname']

        if not indexerName:
            return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer")

        # moved the logic check to the end in an attempt to eliminate empty directory being created from previous errors
        showPath = helpers.generate_show_dir_name(self.location, indexerName)

        # don't create show dir if config says not to
        if sickgear.ADD_SHOWS_WO_DIR:
            self.log(f'Skipping initial creation of {showPath} due to config.ini setting')
        else:
            dir_exists = helpers.make_dir(showPath)
            if not dir_exists:
                self.log(f"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:
                helpers.chmod_as_parent(showPath)

        sickgear.show_queue_scheduler.action.add_show(
            int(self.tvid), int(self.prodid), showPath,
            quality=newQuality, upgrade_once=self.upgradeonce,
            paused=self.pause, default_status=newStatus, scene=self.scene, subtitles=self.subtitles,
            flatten_folders=int(self.flatten_folders), anime=self.anime,
            new_show=True, lang=self.lang
        )

        return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added")


class CMD_ShowAddNew(CMD_SickGearShowAddNew):
    _help = {"desc": "add a new show to sickgear",
             "requiredParameters": {"tvdbid": {"desc": "thetvdb.com id"}
                                    },
             "optionalParameters": {"location": {"desc": "base path for where the show folder is to be created"},
                                    "initial": {"desc": "initial quality for the show"},
                                    "archive": {"desc": "archive quality for the show"},
                                    "pause": {"desc": "pause show search tasks to allow edits"},
                                    "status": {"desc": "status of missing episodes"},
                                    "scene": {"desc": "show searches episodes by scene numbering"},
                                    "subtitles": {"desc": "allow search episode subtitle"},
                                    "flatten_folders": {"desc": "flatten subfolders for the show"},
                                    "anime": {"desc": "set show to anime"},
                                    "lang": {"desc": "the 2 letter lang abbreviation id"}
                                    },
             "SickGearCommand": "sg.show.addnew",
             }

    valid_languages = {
        'el': 20, 'en': 7, 'zh': 27, 'it': 15, 'cs': 28, 'es': 16, 'ru': 22,
        'nl': 13, 'pt': 26, 'no': 9, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31,
        'de': 14, 'da': 10, 'fi': 11, 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32,
        'sv': 8, 'sl': 30}

    def __init__(self, handler, args, kwargs):
        kwargs['indexer'] = TVINFO_TVDB
        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
        self.sickbeard_call = True
        CMD_SickGearShowAddNew.__init__(self, handler, args, kwargs)


class CMD_SickGearShowCache(ApiCall):
    _help = {"desc": "check sickgear's cache to see if the banner or poster image for a show is valid",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ check sickgear's cache to see if the banner or poster image for a show is valid """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # TODO: catch if cache dir is missing/invalid.. so it doesn't break show/show.cache
        # return {"poster": 0, "banner": 0}

        cache_obj = image_cache.ImageCache()

        has_poster = 0
        has_banner = 0

        if os.path.isfile(cache_obj.poster_path(show_obj.tvid, show_obj.prodid)):
            has_poster = 1
        if os.path.isfile(cache_obj.banner_path(show_obj.tvid, show_obj.prodid)):
            has_banner = 1

        return _responds(RESULT_SUCCESS, {"poster": has_poster, "banner": has_banner})


class CMD_ShowCache(CMD_SickGearShowCache):
    _help = {"desc": "check sickgear's cache to see if the banner or poster image for a show is valid",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.cache",
             }

    def __init__(self,
                 handler,
                 args,  # type: List
                 kwargs  # type: Dict
                 ):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowCache.__init__(self, handler, args, kwargs)


class CMD_SickGearShowDelete(ApiCall):
    _help = {"desc": "delete a show from sickgear",
             "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):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, 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)

    def run(self):
        """ delete a show from sickgear """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        if sickgear.show_queue_scheduler.action.is_being_added(
                show_obj) or sickgear.show_queue_scheduler.action.is_being_updated(show_obj):
            return _responds(RESULT_FAILURE, msg="Show can not be deleted while being added or updated")

        show_obj.delete_show(full=self.full_delete)
        return _responds(RESULT_SUCCESS, msg='%s has been deleted' % show_obj.unique_name)


class CMD_ShowDelete(CMD_SickGearShowDelete):
    _help = {"desc": "delete a show from sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.delete",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowDelete.__init__(self, handler, args, kwargs)


class CMD_SickGearShowGetQuality(ApiCall):
    _help = {"desc": "get the quality setting for a show in sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the quality setting for a show in sickgear """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        anyQualities, bestQualities = _mapQuality(show_obj.quality)

        data = {"initial": anyQualities, "archive": bestQualities}

        if not self.sickbeard_call:
            data['upgrade_once'] = show_obj.upgrade_once

        return _responds(RESULT_SUCCESS, data)


class CMD_ShowGetQuality(CMD_SickGearShowGetQuality):
    _help = {"desc": "get the quality setting for a show in sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.getquality",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowGetQuality.__init__(self, handler, args, kwargs)


class CMD_SickGearShowGetPoster(ApiCall):
    _help = {"desc": "get the poster stored for a show in sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the poster for a show in sickgear """
        return {'outputType': 'image', 'image': self.handler.show_poster({self.tvid: self.prodid}, 'poster', True)}


class CMD_ShowGetPoster(CMD_SickGearShowGetPoster):
    _help = {"desc": "get the poster stored for a show in sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.getposter",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowGetPoster.__init__(self, handler, args, kwargs)


class CMD_SickGearShowGetBanner(ApiCall):
    _help = {"desc": "get the banner stored for a show in sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the banner for a show in sickgear """
        return {'outputType': 'image', 'image': self.handler.show_poster({self.tvid: self.prodid}, 'banner', True)}


class CMD_ShowGetBanner(CMD_SickGearShowGetBanner):
    _help = {"desc": "get the banner stored for a show in sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.getbanner",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowGetBanner.__init__(self, handler, args, kwargs)


class CMD_SickGearShowListFanart(ApiCall):
    _help = {"desc": "get list of fanarts stored for a show",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get list of fanarts stored for a show """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        fanart = []
        rating_names = {10: 'group', 20: 'favorite', 30: 'avoid'}
        cache_obj = image_cache.ImageCache()
        for img in glob.glob(cache_obj.fanart_path(show_obj.tvid, show_obj.prodid).replace('fanart.jpg', '*')) or []:
            match = re.search(r'(\d+(?:\.(\w*?(\d*)))?\.(?:\w{5,8}))\.fanart\.', img, re.I)
            if match and match.group(1):
                fanart += [(match.group(1), rating_names.get(sickgear.FANART_RATINGS.get(
                    str(TVidProdid({self.tvid: self.prodid})()), {}).get(match.group(1), ''), ''))]

        return _responds(RESULT_SUCCESS, fanart)


class CMD_SickGearShowRateFanart(ApiCall):
    _help = {"desc": "set a fanart rating",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    "fanartname": {"desc": "fanart name form sg.show.listfanart"},
                                    "rating": {"desc": "rate: unrate, group, favorite, avoid"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        self.fanartname, args = self.check_params(args, kwargs, "fanartname", None, True, "string", [])
        self.rating, args = self.check_params(args, kwargs, "rating", None, True, "string",
                                              ['unrate', 'group', 'favorite', 'avoid'])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ set a fanart rating """
        cache_obj = image_cache.ImageCache()
        fanartfile = cache_obj.fanart_path(self.tvid, self.prodid).replace('fanart.jpg',
                                                                           '%s.fanart.jpg' % self.fanartname)
        if not os.path.isfile(fanartfile):
            return _responds(RESULT_FAILURE, msg='Unknown Fanart')
        fan_ratings = {'unrate': 0, 'group': 10, 'favorite': 20, 'avoid': 30}
        show_id = TVidProdid({self.tvid: self.prodid})()
        if 'unrate' == self.rating and str(show_id) in sickgear.FANART_RATINGS \
                and self.fanartname in sickgear.FANART_RATINGS[str(show_id)]:
            del sickgear.FANART_RATINGS[str(show_id)][self.fanartname]
        else:
            sickgear.FANART_RATINGS.setdefault(str(show_id), {})[self.fanartname] = fan_ratings[self.rating]
        sickgear.save_config()
        return _responds(RESULT_SUCCESS, msg='Rated Fanart: %s = %s' % (self.fanartname, self.rating))


class CMD_SickGearShowGetFanart(ApiCall):
    _help = {"desc": "get the fanart stored for a show"
                     " X-Fanartname response header returns Fanart name or default for not found",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             "optionalParameters": {"fanartname": {"desc": "fanart name form sg.show.listfanart"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        self.fanartname, args = self.check_params(args, kwargs, "fanartname", None, False, "string", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the fanart stored for a show """
        cache_obj = image_cache.ImageCache()
        default_fanartfile = os.path.join(sickgear.PROG_DIR, 'gui', 'slick', 'images', 'trans.png')
        fanartfile = default_fanartfile
        used_fanart = 'default'
        if self.fanartname:
            fanartfile = cache_obj.fanart_path(self.tvid, self.prodid).replace('fanart.jpg',
                                                                               '%s.fanart.jpg' % self.fanartname)
            if not os.path.isfile(fanartfile):
                fanartfile = default_fanartfile
                used_fanart = self.fanartname
        else:
            fanart = []
            for img in glob.glob(cache_obj.fanart_path(self.tvid, self.prodid).replace('fanart.jpg', '*')) or []:
                if not os.path.isfile(img):
                    continue
                match = re.search(r'(\d+(?:\.(\w*?(\d*)))?\.(?:\w{5,8}))\.fanart\.', img, re.I)
                if match and match.group(1):
                    fanart += [(img, match.group(1), sickgear.FANART_RATINGS.get(
                        str(TVidProdid({self.tvid: self.prodid})()), {}).get(match.group(1), 0))]
            if fanart:
                fanartsorted = sorted([f for f in fanart if f[2] != 30], key=lambda x: x[2], reverse=True)
                max_fa = max([f[2] for f in fanartsorted])
                fanartsorted = [f for f in fanartsorted if f[2] == max_fa]
                if fanartsorted:
                    random_fanart = randint(0, len(fanartsorted) - 1)
                    fanartfile = fanartsorted[random_fanart][0]
                    used_fanart = fanartsorted[random_fanart][1]

        if fanartfile and os.path.isfile(fanartfile):
            with open(fanartfile, 'rb') as f:
                mime_type, encoding = MimeTypes().guess_type(fanartfile)
                self.handler.set_header('X-Fanartname', used_fanart)
                self.handler.set_header('Content-Type', mime_type)
                return {'outputType': 'image', 'image': f.read()}

        # we should never get here
        return _responds(RESULT_FAILURE, msg='No Fanart found')


class CMD_SickGearShowPause(ApiCall):
    _help = {"desc": "set the paused state for a show",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             "optionalParameters": {"pause": {"desc": "the pause state to set"}
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        self.pause, args = self.check_params(args, kwargs, "pause", 0, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ set the paused state for a show """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        if self.pause:
            show_obj.paused = True
            show_obj.save_to_db()
            return _responds(RESULT_SUCCESS, msg='%s has been paused' % show_obj.unique_name)
        else:
            show_obj.paused = False
            show_obj.save_to_db()
            return _responds(RESULT_SUCCESS, msg='%s has been unpaused' % show_obj.unique_name)

        # return _responds(RESULT_FAILURE, msg=str(show_obj.name) + " was unable to be paused")


class CMD_ShowPause(CMD_SickGearShowPause):
    _help = {"desc": "set the paused state for a show",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "optionalParameters": {"pause": {"desc": "the pause state to set"}
                                    },
             "SickGearCommand": "sg.show.pause",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowPause.__init__(self, handler, args, kwargs)


class CMD_SickGearShowRefresh(ApiCall):
    _help = {"desc": "refresh a show in sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"}, },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ refresh a show in sickgear """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        try:
            sickgear.show_queue_scheduler.action.refresh_show(show_obj)
            return _responds(RESULT_SUCCESS, msg='%s has queued to be refreshed' % show_obj.unique_name)
        except exceptions_helper.CantRefreshException as e:
            # TODO: log the exception
            return _responds(RESULT_FAILURE, msg='Unable to refresh %s. %s' % (show_obj.unique_name, ex(e)))


class CMD_ShowRefresh(CMD_SickGearShowRefresh):
    _help = {"desc": "refresh a show in sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.refresh",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowRefresh.__init__(self, handler, args, kwargs)


class CMD_SickGearShowSeasonList(ApiCall):
    _help = {"desc": "get a season list for a show",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             "optionalParameters": {"sort": {"desc": "change the sort order from descending to ascending"}}
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        self.sort, args = self.check_params(args, kwargs, "sort", "desc", False, "string",
                                            ["asc", "desc"])  # "asc" and "desc" default and fallback is "desc"
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get a season list for a show """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        my_db = db.DBConnection(row_type="dict")
        if "asc" == self.sort:
            # noinspection SqlRedundantOrderingDirection
            sql_result = my_db.select(
                "SELECT DISTINCT season"
                " FROM tv_episodes"
                " WHERE indexer = ? AND showid = ?"
                " ORDER BY season ASC",
                [self.tvid, self.prodid])
        else:
            sql_result = my_db.select(
                "SELECT DISTINCT season"
                " FROM tv_episodes"
                " WHERE indexer = ? AND showid = ?"
                " ORDER BY season DESC",
                [self.tvid, self.prodid])
        seasonList = []  # a list with all season numbers
        for cur_result in sql_result:
            seasonList.append(int(cur_result["season"]))

        return _responds(RESULT_SUCCESS, seasonList)


class CMD_ShowSeasonList(CMD_SickGearShowSeasonList):
    _help = {"desc": "get a season list for a show",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "optionalParameters": {"sort": {"desc": "change the sort order from descending to ascending"}
                                    },
             "SickGearCommand": "sg.show.seasonlist",
             }

    def __init__(self,
                 handler,
                 args,  # type: List
                 kwargs  # type: Dict
                 ):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowSeasonList.__init__(self, handler, args, kwargs)


class CMD_SickGearShowSeasons(ApiCall):
    _help = {"desc": "get episode list of all or a specific season",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             "optionalParameters": {"season": {"desc": "the season number"},
                                    }
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        self.season, args = self.check_params(args, kwargs, "season", None, False, "int", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get episode list of all or a specific season """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        my_db = db.DBConnection(row_type="dict")

        if None is self.season:
            sql_result = my_db.select(
                "SELECT name, description, absolute_number, scene_absolute_number, episode,"
                " scene_episode, scene_season, airdate, status, season"
                " FROM tv_episodes"
                " WHERE indexer = ? AND showid = ?",
                [self.tvid, self.prodid])
            seasons = {}  # type: Dict[int, Dict]
            for cur_result in sql_result:
                status, quality = Quality.split_composite_status(int(cur_result["status"]))
                cur_result["status"] = _get_status_Strings(status)
                cur_result["quality"] = _get_quality_string(quality)
                timezone, cur_result['timezone'] = network_timezones.get_network_timezone(show_obj.network,
                                                                                          return_name=True)
                dtEpisodeAirs = SGDatetime.convert_to_setting(
                    network_timezones.parse_date_time(cur_result['airdate'], show_obj.airs, timezone))
                cur_result['airdate'] = SGDatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat)
                cur_result['scene_episode'] = helpers.try_int(cur_result['scene_episode'])
                cur_result['scene_season'] = helpers.try_int(cur_result['scene_season'])
                cur_result['absolute_number'] = helpers.try_int(cur_result['absolute_number'])
                cur_result['scene_absolute_number'] = helpers.try_int(cur_result['scene_absolute_number'])
                curSeason = int(cur_result["season"])
                curEpisode = int(cur_result["episode"])
                del cur_result["season"]
                del cur_result["episode"]
                if curSeason not in seasons:
                    seasons[curSeason] = {}
                seasons[curSeason][curEpisode] = cur_result

        else:
            sql_result = my_db.select(
                "SELECT name, description, absolute_number, scene_absolute_number,"
                " episode, scene_episode, scene_season, airdate, status"
                " FROM tv_episodes"
                " WHERE indexer = ? AND showid = ? AND season = ?",
                [self.tvid, self.prodid, self.season])
            if len(sql_result) == 0:
                return _responds(RESULT_FAILURE, msg="Season not found")
            seasons = {}
            for cur_result in sql_result:
                curEpisode = int(cur_result["episode"])
                del cur_result["episode"]
                status, quality = Quality.split_composite_status(int(cur_result["status"]))
                cur_result["status"] = _get_status_Strings(status)
                cur_result["quality"] = _get_quality_string(quality)
                timezone, cur_result['timezone'] = network_timezones.get_network_timezone(show_obj.network,
                                                                                          return_name=True)
                dtEpisodeAirs = SGDatetime.convert_to_setting(
                    network_timezones.parse_date_time(cur_result['airdate'], show_obj.airs, timezone))
                cur_result['airdate'] = SGDatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat)
                cur_result['scene_episode'] = helpers.try_int(cur_result['scene_episode'])
                cur_result['scene_season'] = helpers.try_int(cur_result['scene_season'])
                cur_result['absolute_number'] = helpers.try_int(cur_result['absolute_number'])
                cur_result['scene_absolute_number'] = helpers.try_int(cur_result['scene_absolute_number'])
                if curEpisode not in seasons:
                    seasons[curEpisode] = {}
                seasons[curEpisode] = cur_result

        return _responds(RESULT_SUCCESS, seasons)


class CMD_ShowSeasons(CMD_SickGearShowSeasons):
    _help = {"desc": "get episode list of all or a specific season",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "optionalParameters": {"season": {"desc": "the season number"},
                                    },
             "SickGearCommand": "sg.show.seasons",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowSeasons.__init__(self, handler, args, kwargs)


class CMD_SickGearShowSetQuality(ApiCall):
    _help = {
        "desc": "set desired quality of a show in sickgear"
                " if neither initial or archive are provided then the config default quality will be used",
        "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                               "indexerid": {"desc": "unique id of a show"}},
        "optionalParameters": {"initial": {"desc": "initial quality for the show"},
                               "archive": {"desc": "archive quality for the show"},
                               "upgrade_once": {"desc": "upgrade only once"}}
            }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # this for whatever reason removes hdbluray not sdtv... which is just wrong.
        # reverting to previous code.. plus we didn't use the new code everywhere.
        # self.archive, args = self.check_params(
        # args, kwargs, "archive", None, False, "list", _getQualityMap().values()[1:])
        self.initial, args = self.check_params(args, kwargs, "initial", None, False, "list", [q for q in quality_map])
        self.archive, args = self.check_params(args, kwargs, "archive", None, False, "list", [q for q in quality_map])
        self.upgradeonce, args = self.check_params(args, kwargs, "upgrade_once", False, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ set the quality for a show in sickgear by taking in a deliminated
            string of qualities, map to their value and combine for new values
        """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # use default quality as a failsafe
        newQuality = int(sickgear.QUALITY_DEFAULT)
        iqualityID = []
        aqualityID = []

        if self.initial:
            for quality in self.initial:
                iqualityID.append(quality_map[quality])
        if self.archive:
            for quality in self.archive:
                aqualityID.append(quality_map[quality])

        if iqualityID or aqualityID:
            newQuality = Quality.combine_qualities(iqualityID, aqualityID)
        show_obj.quality = newQuality

        show_obj.upgrade_once = self.upgradeonce

        show_obj.save_to_db()

        return _responds(RESULT_SUCCESS,
                         msg='%s quality has been changed to %s' % (show_obj.unique_name,
                                                                    _get_quality_string(show_obj.quality)))


class CMD_ShowSetQuality(CMD_SickGearShowSetQuality):
    _help = {
        "desc": "set desired quality of a show in sickgear"
                " if neither initial or archive are provided then the config default quality will be used",
        "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"}},
        "optionalParameters": {"initial": {"desc": "initial quality for the show"},
                               "archive": {"desc": "archive quality for the show"}},
        "SickGearCommand": "sg.show.setquality",
            }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help

        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowSetQuality.__init__(self, handler, args, kwargs)


class CMD_SickGearShowStats(ApiCall):
    _help = {"desc": "get episode statistics for a show",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get episode statistics for a given show """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        # show stats
        episode_status_counts_total = {"total": 0}
        for status in statusStrings.statusStrings:
            if status in SNATCHED_ANY + [UNKNOWN, DOWNLOADED]:
                continue
            episode_status_counts_total[status] = 0

        # add all the downloaded qualities
        episode_qualities_counts_download = {"total": 0}
        for statusCode in Quality.DOWNLOADED:
            status, quality = Quality.split_composite_status(statusCode)
            if quality in [Quality.NONE]:
                continue
            episode_qualities_counts_download[statusCode] = 0

        # add all snatched qualities
        episode_qualities_counts_snatch = {"total": 0}
        for statusCode in Quality.SNATCHED_ANY:
            status, quality = Quality.split_composite_status(statusCode)
            if quality in [Quality.NONE]:
                continue
            episode_qualities_counts_snatch[statusCode] = 0

        my_db = db.DBConnection(row_type="dict")
        sql_result = my_db.select("SELECT status, season FROM tv_episodes WHERE season != 0 AND showid = ? "
                                  "AND indexer = ?",
                                  [self.prodid, self.tvid])
        # the main loop that goes through all episodes
        for cur_result in sql_result:
            status, quality = Quality.split_composite_status(int(cur_result["status"]))

            episode_status_counts_total["total"] += 1

            if status in Quality.DOWNLOADED:
                episode_qualities_counts_download["total"] += 1
                episode_qualities_counts_download[int(cur_result["status"])] += 1
            elif status in Quality.SNATCHED_ANY:
                episode_qualities_counts_snatch["total"] += 1
                episode_qualities_counts_snatch[int(cur_result["status"])] += 1
            elif 0 == status:  # we dont count NONE = 0 = N/A
                pass
            else:
                episode_status_counts_total[status] += 1

        # the outgoing container
        episodes_stats = {"downloaded": {}}
        # turning codes into strings
        for statusCode in episode_qualities_counts_download:
            if "total" == statusCode:
                episodes_stats["downloaded"]["total"] = episode_qualities_counts_download[statusCode]
                continue
            status, quality = Quality.split_composite_status(int(statusCode))
            statusString = Quality.qualityStrings[quality].lower().replace(" ", "_").replace("(", "").replace(")", "")
            episodes_stats["downloaded"][statusString] = episode_qualities_counts_download[statusCode]

        episodes_stats["snatched"] = {}
        # turning codes into strings
        # and combining proper and normal
        for statusCode in episode_qualities_counts_snatch:
            if "total" == statusCode:
                episodes_stats["snatched"]["total"] = episode_qualities_counts_snatch[statusCode]
                continue
            status, quality = Quality.split_composite_status(int(statusCode))
            statusString = Quality.qualityStrings[quality].lower().replace(" ", "_").replace("(", "").replace(")", "")
            if Quality.qualityStrings[quality] in episodes_stats["snatched"]:
                episodes_stats["snatched"][statusString] += episode_qualities_counts_snatch[statusCode]
            else:
                episodes_stats["snatched"][statusString] = episode_qualities_counts_snatch[statusCode]

        # episodes_stats["total"] = {}
        for statusCode in episode_status_counts_total:
            if "total" == statusCode:
                episodes_stats["total"] = episode_status_counts_total[statusCode]
                continue
            status, quality = Quality.split_composite_status(int(statusCode))
            statusString = statusStrings.statusStrings[statusCode].lower().replace(" ", "_").replace("(", "").replace(
                ")", "")
            episodes_stats[statusString] = episode_status_counts_total[statusCode]

        return _responds(RESULT_SUCCESS, episodes_stats)


class CMD_ShowStats(CMD_SickGearShowStats):
    _help = {"desc": "get episode statistics for a show",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.stats",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowStats.__init__(self, handler, args, kwargs)


class CMD_SickGearShowUpdate(ApiCall):
    _help = {"desc": "update a show in sickgear",
             "requiredParameters": {"indexer": {"desc": "indexer of a show"},
                                    "indexerid": {"desc": "unique id of a show"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.tvid, args = self.check_params(args, kwargs, "indexer", None, True, "int",
                                            [i for i in indexer_api.TVInfoAPI().search_sources])
        self.prodid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", [])
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ update a show in sickgear """
        show_obj = helpers.find_show_by_id({self.tvid: self.prodid})
        if not show_obj:
            return _responds(RESULT_FAILURE, msg="Show not found")

        try:
            sickgear.show_queue_scheduler.action.update_show(show_obj, True)
            return _responds(RESULT_SUCCESS, msg='%s has queued to be updated' % show_obj.unique_name)
        except exceptions_helper.CantUpdateException as e:
            self.log(f'Unable to update {show_obj.unique_name}. {ex(e)}', logger.ERROR)
            return _responds(RESULT_FAILURE, msg='Unable to update %s. %s' % (show_obj.unique_name, ex(e)))


class CMD_ShowUpdate(CMD_SickGearShowUpdate):
    _help = {"desc": "update a show in sickgear",
             "requiredParameters": {"indexerid": {"desc": "thetvdb.com id of a show"},
                                    },
             "SickGearCommand": "sg.show.update",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        kwargs['indexer'] = TVINFO_TVDB
        self.sickbeard_call = True
        CMD_SickGearShowUpdate.__init__(self, handler, args, kwargs)


class CMD_SickGearShows(ApiCall):
    _help = {"desc": "get all shows in sickgear",
             "optionalParameters": {"sort": {"desc": "sort the list of shows by show name instead of indexerid"},
                                    "paused": {"desc": "only return shows that are set to paused"},
                                    "overview": {"desc": "include overview"},
                                    },
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        self.sort, args = self.check_params(args, kwargs, "sort", "id", False, "string", ["id", "name"])
        self.paused, args = self.check_params(args, kwargs, "paused", None, False, "bool", [])
        self.overview, args = self.check_params(args, kwargs, "overview", False, False, "bool", [])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ display_is_int_multi( self.prodid ) shows in sickgear """
        shows = {}

        for cur_show_obj in sickgear.showList:

            if self.sickbeard_call and TVINFO_TVDB != cur_show_obj.tvid:
                continue

            if None is not self.paused and bool(self.paused) != bool(cur_show_obj.paused):
                continue

            genreList = []
            if cur_show_obj.genre:
                genreListTmp = cur_show_obj.genre.split("|")
                for genre in genreListTmp:
                    if genre:
                        genreList.append(genre)
            anyQualities, bestQualities = _mapQuality(cur_show_obj.quality)

            showDict = {
                "paused": cur_show_obj.paused,
                "quality": _get_quality_string(cur_show_obj.quality),
                "language": cur_show_obj.lang,
                "air_by_date": cur_show_obj.air_by_date,
                "airs": cur_show_obj.airs,
                "flatten_folders": cur_show_obj.flatten_folders,
                "genre": genreList,
                "location": cur_show_obj._location,
                "quality_details": {"initial": anyQualities, "archive": bestQualities},
                "sports": cur_show_obj.sports,
                "anime": cur_show_obj.anime,
                "indexerid": cur_show_obj.prodid,
                "indexer": cur_show_obj.tvid,
                "tvdbid": cur_show_obj.ids.get(TVINFO_TVDB, {'id': 0})['id'],
                'ids': {k: v.get('id') for k, v in iteritems(cur_show_obj.ids)},
                "tvrage_id": cur_show_obj.ids.get(TVINFO_TVRAGE, {'id': 0})['id'],
                "tvrage_name": cur_show_obj.name,
                "network": cur_show_obj.network,
                "show_name": cur_show_obj.name,
                "status": cur_show_obj.status,
                "subtitles": cur_show_obj.subtitles,
                "scenenumbering": cur_show_obj.is_scene,
                "upgrade_once": cur_show_obj.upgrade_once,
                "ignorewords": helpers.generate_word_str(cur_show_obj.rls_ignore_words, cur_show_obj.rls_ignore_words_regex),
                "global_exclude_ignore": helpers.generate_word_str(cur_show_obj.rls_global_exclude_ignore),
                "requirewords": helpers.generate_word_str(cur_show_obj.rls_require_words, cur_show_obj.rls_require_words_regex),
                "global_exclude_require": helpers.generate_word_str(cur_show_obj.rls_global_exclude_require),
                "prune": cur_show_obj.prune,
                "tag": cur_show_obj.tag,
                "imdb_id": cur_show_obj.imdbid,
                "classification": cur_show_obj.classification,
                "runtime": cur_show_obj.runtime,
                "startyear": cur_show_obj.startyear,
            }

            if self.overview:
                showDict["overview"] = cur_show_obj.overview

            timezone, showDict['timezone'] = network_timezones.get_network_timezone(showDict['network'],
                                                                                    return_name=True)

            if cur_show_obj.next_episode():
                dtEpisodeAirs = SGDatetime.convert_to_setting(
                    network_timezones.parse_date_time(cur_show_obj.nextaired, cur_show_obj.airs, timezone))
                showDict['next_ep_airdate'] = SGDatetime.sbfdate(dtEpisodeAirs, d_preset=dateFormat)
            else:
                showDict['next_ep_airdate'] = ''

            showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": cur_show_obj.tvid,
                                                                         "indexerid": cur_show_obj.prodid}).run()["data"]
            if not showDict["network"]:
                showDict["network"] = ""
            if "name" == self.sort:
                shows[cur_show_obj.name] = showDict
            else:
                if self.sickbeard_call:
                    shows[cur_show_obj.prodid] = showDict
                else:
                    shows[cur_show_obj.tvid_prodid] = showDict

        return _responds(RESULT_SUCCESS, shows)


class CMD_Shows(CMD_SickGearShows):
    _help = {"desc": "get all thetvdb.com shows in sickgear",
             "optionalParameters": {"sort": {"desc": "sort the list of shows by show name instead of indexerid"},
                                    "paused": {"desc": "only show the shows that are set to paused"},
                                    "overview": {"desc": "include overview"},
                                    },
             "SickGearCommand": "sg.shows",
             }

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearShows.__init__(self, handler, args, kwargs)


class CMD_SickGearShowsBrowseTrakt(ApiCall):
    _help = {"desc": "browse trakt shows in sickgear",
             "requiredParameters": {"type": {"desc": "type to browse: anticipated, newshows, newseasons, popular, "
                                                     "trending, recommended, watchlist"},
                                    },
             "optionalParameters": {"account_id": {"desc": "account_id for recommended, watchlist - "
                                                           "see sg.listtraktaccounts"}},
             }

    def __init__(self, handler, args, kwargs):
        # required
        self.type, args = self.check_params(args, kwargs, "type", "anticipated", True, "string",
                                            ["anticipated", "newshows", "newseasons", "popular", "trending",
                                             "recommended", "watchlist"])
        # optional
        self.account, args = self.check_params(args, kwargs, "account_id", None, False, "int",
                                               [s for s in sickgear.TRAKT_ACCOUNTS])
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ browse trakt shows in sickgear """
        func = dict(anticipated='get_anticipated',
                    newshows='get_new_shows',
                    newseasons='get_new_seasons',
                    popular='get_popular',
                    trending='get_trending')
        kwargs = {}
        if self.type in ('recommended', 'watchlist'):
            if not self.account:
                return _responds(RESULT_FAILURE, msg='Need Trakt account')
            func.update(dict(recommended='get_recommended_for_account',
                             watchlist='get_watchlisted_for_account'))
            kwargs.update(dict(account=self.account, ignore_collected=True))
            if self.type in ('recommended',):
                kwargs.update(dict(ignore_watchlisted=True))
        try:
            data, oldest, newest = AddShows.get_trakt_data(func[self.type], **kwargs)
        except Exception as e:
            return _responds(RESULT_FAILURE, msg=ex(e))
        return _responds(RESULT_SUCCESS, data)


class CMD_SickGearListTraktAccounts(ApiCall):
    _help = {"desc": "list Trakt accounts in sickgear"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ list Trakt accounts in sickgear """
        accounts = [{'name': v.name, 'account_id': v.account_id} for a, v in iteritems(sickgear.TRAKT_ACCOUNTS)]
        return _responds(RESULT_SUCCESS, accounts)


class CMD_SickGearShowsForceUpdate(ApiCall):
    _help = {"desc": "force the daily show update now"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ force the daily show update now """
        if sickgear.show_queue_scheduler.action.is_show_update_running() \
                or sickgear.update_show_scheduler.is_running_job:
            return _responds(RESULT_FAILURE, msg="show update already running.")

        result = sickgear.update_show_scheduler.force_run()
        if result:
            return _responds(RESULT_SUCCESS, msg="daily show update started")
        return _responds(RESULT_FAILURE, msg="can't start show update currently")


class CMD_SickGearShowsQueue(ApiCall):
    _help = {"desc": "list the show update queue"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ list the show update queue """
        return _responds(RESULT_SUCCESS, sickgear.show_queue_scheduler.action.queue_length())


class CMD_SickGearShowsStats(ApiCall):
    _help = {"desc": "get the global shows and episode stats"}

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        ApiCall.__init__(self, handler, args, kwargs)

    def run(self):
        """ get the global shows and episode stats """
        stats = {}

        indexer_limit = ('', ' AND indexer = %s' % TVINFO_TVDB)[self.sickbeard_call]
        my_db = db.DBConnection()
        today = str(datetime.date.today().toordinal())

        stats["shows_total"] = (len(sickgear.showList),
                                len([cur_so for cur_so in sickgear.showList
                                     if TVINFO_TVDB == cur_so.tvid]))[self.sickbeard_call]

        stats["shows_active"] = len(
            [cur_so for cur_so in sickgear.showList
             if 0 == cur_so.paused and "Ended" != cur_so.status
             and (not self.sickbeard_call
                  or TVINFO_TVDB == cur_so.tvid)])

        stats["ep_downloaded"] = my_db.select("SELECT COUNT(*) FROM tv_episodes WHERE status IN (" + ",".join(
            [str(status) for status in
             Quality.DOWNLOADED + Quality.ARCHIVED]) + ") AND season != 0 and episode != 0 AND airdate <= " + today +
                                             indexer_limit)[0][0]

        stats["ep_total"] = my_db.select(
            "SELECT COUNT(*) FROM tv_episodes WHERE season != 0 AND episode != 0 AND (airdate != 1 OR status IN (" +
            ",".join([str(status) for status in Quality.SNATCHED_ANY + Quality.DOWNLOADED + Quality.ARCHIVED]) +
            ")) AND airdate <= " + today + " AND status != " + str(IGNORED) + indexer_limit)[0][0]

        return _responds(RESULT_SUCCESS, stats)


# WARNING: never define a cmd call string that contains a "_" (underscore)
class CMD_ShowsStats(CMD_SickGearShowsStats):
    _help = {"desc": "get the global thetvdb.com shows and episode stats",
             "SickGearCommand": "sg.shows.stats"}
# this is reserved for cmd indexes used while cmd chaining

    def __init__(self, handler, args, kwargs):
        # required
        # optional
        # super, missing, help
        self.sickbeard_call = True
        CMD_SickGearShowsStats.__init__(self, handler, args, kwargs)

# WARNING: never define a param name that contains a "." (dot)
# this is reserved for cmd namspaces used while cmd chaining
# this is reserved for cmd indexes used while cmd chaining

# WARNING: never define a param name that contains a "." (dot)
# this is reserved for cmd namspaces used while cmd chaining


_functionMaper = {"help": CMD_Help,
                  "listcommands": CMD_ListCommands,
                  "future": CMD_ComingEpisodes,
                  "sg.future": CMD_SickGearComingEpisodes,
                  "episode": CMD_Episode,
                  "sg.episode": CMD_SickGearEpisode,
                  "episode.search": CMD_EpisodeSearch,
                  "sg.episode.search": CMD_SickGearEpisodeSearch,
                  "episode.setstatus": CMD_EpisodeSetStatus,
                  "sg.episode.setstatus": CMD_SickGearEpisodeSetStatus,
                  "episode.subtitlesearch": CMD_SubtitleSearch,
                  "sg.episode.subtitlesearch": CMD_SickGearSubtitleSearch,
                  "exceptions": CMD_Exceptions,
                  "sg.exceptions": CMD_SickGearExceptions,
                  'sg.setexceptions': CMD_SetExceptions,
                  "history": CMD_History,
                  "sg.history": CMD_SickGearHistory,
                  "history.clear": CMD_HistoryClear,
                  "sg.history.clear": CMD_SickGearHistoryClear,
                  "history.trim": CMD_HistoryTrim,
                  "sg.history.trim": CMD_SickGearHistoryTrim,
                  "logs": CMD_Logs,
                  "sg.logs": CMD_SickGearLogs,
                  "sb": CMD_SickBeard,
                  "sg": CMD_SickGear,
                  "postprocess": CMD_PostProcess,
                  "sg.postprocess": CMD_SickGearPostProcess,
                  "sb.addrootdir": CMD_SickBeardAddRootDir,
                  "sg.addrootdir": CMD_SickGearAddRootDir,
                  "sb.checkscheduler": CMD_SickBeardCheckScheduler,
                  "sg.checkscheduler": CMD_SickGearCheckScheduler,
                  "sb.deleterootdir": CMD_SickBeardDeleteRootDir,
                  "sg.deleterootdir": CMD_SickGearDeleteRootDir,
                  "sb.forcesearch": CMD_SickBeardForceSearch,
                  "sg.forcesearch": CMD_SickGearForceSearch,
                  "sg.searchqueue": CMD_SickGearSearchQueue,
                  "sb.getdefaults": CMD_SickBeardGetDefaults,
                  "sg.getdefaults": CMD_SickGearGetDefaults,
                  "sb.getmessages": CMD_SickBeardGetMessages,
                  "sg.getmessages": CMD_SickGearGetMessages,
                  "sg.getqualities": CMD_SickGearGetQualities,
                  "sg.getqualitystrings": CMD_SickGearGetqualityStrings,
                  "sb.getrootdirs": CMD_SickBeardGetRootDirs,
                  "sg.getrootdirs": CMD_SickGearGetRootDirs,
                  "sb.pausebacklog": CMD_SickBeardPauseBacklog,
                  "sg.pausebacklog": CMD_SickGearPauseBacklog,
                  "sb.ping": CMD_SickBeardPing,
                  "sg.ping": CMD_SickGearPing,
                  "sb.restart": CMD_SickBeardRestart,
                  "sg.restart": CMD_SickGearRestart,
                  "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_old,  # old method name
                  "sg.setrequiredwords": CMD_SickGearSetRequireWords_old,  # old method name
                  "sg.listrequirewords": CMD_SickGearListRequireWords,
                  "sg.setrequirewords": CMD_SickGearSetRequireWords,
                  "sg.updatewatchedstate": CMD_SickGearUpdateWatchedState,
                  "show": CMD_Show,
                  "sg.show": CMD_SickGearShow,
                  "show.addexisting": CMD_ShowAddExisting,
                  "sg.show.addexisting": CMD_SickGearShowAddExisting,
                  "show.addnew": CMD_ShowAddNew,
                  "sg.show.addnew": CMD_SickGearShowAddNew,
                  "show.cache": CMD_ShowCache,
                  "sg.show.cache": CMD_SickGearShowCache,
                  "show.delete": CMD_ShowDelete,
                  "sg.show.delete": CMD_SickGearShowDelete,
                  "show.getquality": CMD_ShowGetQuality,
                  "sg.show.getquality": CMD_SickGearShowGetQuality,
                  "show.getposter": CMD_ShowGetPoster,
                  "sg.show.getposter": CMD_SickGearShowGetPoster,
                  "show.getbanner": CMD_ShowGetBanner,
                  "sg.show.getbanner": CMD_SickGearShowGetBanner,
                  "sg.show.listfanart": CMD_SickGearShowListFanart,
                  "sg.show.ratefanart": CMD_SickGearShowRateFanart,
                  "sg.show.getfanart": CMD_SickGearShowGetFanart,
                  "show.pause": CMD_ShowPause,
                  "sg.show.pause": CMD_SickGearShowPause,
                  "show.refresh": CMD_ShowRefresh,
                  "sg.show.refresh": CMD_SickGearShowRefresh,
                  "show.seasonlist": CMD_ShowSeasonList,
                  "sg.show.seasonlist": CMD_SickGearShowSeasonList,
                  "show.seasons": CMD_ShowSeasons,
                  "sg.show.seasons": CMD_SickGearShowSeasons,
                  "show.setquality": CMD_ShowSetQuality,
                  "sg.show.setquality": CMD_SickGearShowSetQuality,
                  "show.stats": CMD_ShowStats,
                  "sg.show.stats": CMD_SickGearShowStats,
                  "show.update": CMD_ShowUpdate,
                  "sg.show.update": CMD_SickGearShowUpdate,
                  "shows": CMD_Shows,
                  "sg.shows": CMD_SickGearShows,
                  "sg.shows.browsetrakt": CMD_SickGearShowsBrowseTrakt,
                  "sg.listtraktaccounts": CMD_SickGearListTraktAccounts,
                  "shows.stats": CMD_ShowsStats,
                  "sg.shows.stats": CMD_SickGearShowsStats,
                  "sg.shows.forceupdate": CMD_SickGearShowsForceUpdate,
                  "sg.shows.queue": CMD_SickGearShowsQueue,
                  }

_functionMaper_reversed = {v: k for k, v in iteritems(_functionMaper)}
