# -*- coding: utf-8 -*-

"""
Provides base classes for accessing MythTV
"""

from MythTV.static import *
from MythTV.exceptions import *
from MythTV.logging import MythLog
from MythTV.connections import FEConnection, XMLConnection, BEEventConnection
from MythTV.utility import databaseSearch, datetime, check_ipv6, _donothing, resolve_ip
from MythTV.database import DBCache, DBData
from MythTV.system import SystemEvent
from MythTV.mythproto import BECache, FileOps, Program, FreeSpace, EventLock
from MythTV.dataheap import *

from datetime import timedelta
from weakref import proxy
from collections import namedtuple
from urllib.request import urlopen
import re


class CaptureCard( DBData ):
    pass

class MythBE( FileOps ):
    __doc__ = FileOps.__doc__+"""
        getPendingRecordings()    - returns a list of scheduled recordings
        getScheduledRecordings()  - returns a list of scheduled recordings
        getUpcomingRecordings()   - returns a list of scheduled recordings
        getConflictedRecordings() - returns a list of conflicting recordings
        getRecorderList()         - returns a list of all recorder ids
        getFreeRecorderList()     - returns a list of free recorder ids
        lockTuner()               - requests a lock of a recorder
        freeTuner()               - requests an unlock of a recorder
        getCheckfile()            - returns the location of a recording
        getExpiring()             - returns a list of expiring recordings
        getFreeSpace()            - returns a list of FreeSpace objects
        getFreeSpaceSummary()     - returns a tuple of total and used space
        getLastGuideData()        - returns the last date of guide data
        getLoad()                 - returns a tuple of load averages
        getRecordings()           - returns a list of all recordings
        getSGFile()               - returns information on a single file
        getSGList()               - returns lists of directories,
                                    files, and sizes
        getUptime()               - returns system uptime in seconds
        isActiveBackend()         - determines whether backend is
                                    currently active
        isRecording()             - determinds whether recorder is
                                    currently recording
        walkSG()                  - walks a storage group tree, similarly
                                    to os.walk(). returns a tuple of dirnames
                                    and dictionary of filenames with sizes
    """

    locked_tuners = {}

    # Prepare tuple for backend command 'GET_FREE_INPUT_INFO'
    _i_info = ('name', 'sourceid', 'inputid', 'mplexid', 'chanid', 'displayName',
               'recPriority', 'scheduleOrder', 'livetvorder', 'quickTune')
    _InputInfo = namedtuple('InputInfo', _i_info)

    def __del__(self):
        self.freeTuner()

    @FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True)
    def getPendingRecordings(self, pg):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        return pg

    @FileOps._ProgramQuery('QUERY_GETALLSCHEDULED', sorted=True)
    def getScheduledRecordings(self, pg):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        return pg

    @FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True,
                           recstatus=Program.rsWillRecord)
    def getUpcomingRecordings(self, pg):
        """
        Returns a list of Program objects which are scheduled to be recorded.

        Sorts the list by recording start time and only returns those with
        record status of WillRecord.
        """
        return pg

    @FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True,
                           recstatus=Program.rsConflict)
    def getConflictedRecordings(self, pg):
        """
        Retuns a list of Program objects subject to conflicts in the schedule.
        """
        return pg

    def getRecorderList(self):
        """
        Returns a list of recorders, or an empty list if none.
        """
        recorders = []
        with self.db.cursor(self.log) as cursor:
            cursor.execute('SELECT cardid FROM capturecard')
            for row in cursor:
                recorders.append(int(row[0]))
        return recorders

    def getFreeRecorderList(self):
        """
        Returns a list of free recorders in preferred live TV order,
        or an empty list if none.
        """
        res = self.backendCommand('GET_FREE_INPUT_INFO 0').split(BACKEND_SEP)
        # this maps the list of variable length ('res') to InputInfo tuples:
        res_inputinfo = \
            [self._InputInfo(*el) for el in zip(*[iter(res)]*len(self._i_info))]
        recorders = [x.inputid for x in res_inputinfo]
        return recorders

    def getFreeInputInfo(self):
        """
        Return a list of 'InputInfo' tuples of free recorders in preferred
        live TV order.
        Returns an empty list if no recorders are available.
        Introduced in protocol 87, changed in protocols 89, 90, 91.

        InputInfo is a named tuple containing:
        ('InputInfo', ('name', 'sourceid', 'inputid', 'mplexid', 'chanid',
                       'displayName', 'recPriority', 'scheduleOrder',
                       'livetvorder', 'quickTune')).

        See definition of InputInfo in inputinfo.h.

        Usage examples:

        MythBE.getFreeInputInfo()[3].displayName   ---> 'Eingang 13:MPEG2TS'

        [x.inputid for x in getFreeInputInfo()]   --->  list of free recorders
        """
        res = self.backendCommand('GET_FREE_INPUT_INFO 0').split(BACKEND_SEP)
        # this maps the list of variable length ('res') to InputInfo tuples:
        res_inputinfo = \
            [self._InputInfo(*el) for el in zip(*[iter(res)]*len(self._i_info))]
        return res_inputinfo

    def lockTuner(self,id=None):
        """
        Request a tuner be locked from use, optionally specifying which tuner
        Returns a tuple of ID, video dev node, audio dev node, vbi dev node
        Returns an ID of -2 if tuner is locked
                         -1 if no tuner could be found
        """
        cmd = 'LOCK_TUNER'
        be = self

        if id is not None:
            card = None
            try:
                # pull information from database to confirm existance
                card = CaptureCard(id)
            except MythError:
                raise MythError("Capture card %s not found" % id)

            cmd += ' %d' % id
            if card.hostname != be.hostname:
                # connect to slave backend if needed
                be = MythBE(card.hostname, db=self.db)

        res = be.backendCommand(cmd).split(BACKEND_SEP)
        err = int(res[0])

        if err > 0:
            # success, store tuner and return device nodes
            self.locked_tuners[err] = be.hostname
            return tuple(res[1:])

        # return failure mode
        return err

    def freeTuner(self,id=None):
        """
        Frees a requested tuner ID
        If no ID given, free all tuners listed as used by this class instance
        """
        tunerlist = {}

        if id is not None:
            id = int(id)
            if id in self.locked_tuners:
                # tuner is known, pop from list
                tunerlist[id] = self.locked_tuners.pop(id)

            else:
                # tuner is not known, find hostname
                try:
                    card = CaptureCard(id)
                except MythError:
                    raise MythError("Capture card %s not found" % id)
                tunerlist[id] = card.hostname
        else:
            # use the stored list
            tunerlist = self.locked_tuners

        hosts = {self.hostname:self}

        while True:
            try:
                # get a tuner
                id, host = tunerlist.popitem()

                # get the backend connection
                be = None
                if host in hosts:
                    be = hosts[host]
                else:
                    be = MythBE(host, db=self.db)
                    hosts[host] = be

                # unlock the tuner
                be.backendCommand('FREE_TUNER %d' % id)
            except KeyError:
                # out of tuners
                break

    def getCurrentRecording(self, recorder):
        """
        Returns a Program object for the current recorders recording.
        """
        res = self.backendCommand('QUERY_RECORDER '+
                BACKEND_SEP.join([str(recorder),'GET_CURRENT_RECORDING']))
        return Program(res.split(BACKEND_SEP), db=self.db)

    def isRecording(self, recorder):
        """
        Returns a boolean as to whether the given recorder is recording.
        """
        res = self.backendCommand('QUERY_RECORDER '+
                BACKEND_SEP.join([str(recorder),'IS_RECORDING']))
        if res == '1':
            return True
        else:
            return False

    def isActiveBackend(self, hostname):
        """
        Returns a boolean as to whether the given host is an active backend
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                    ['QUERY_IS_ACTIVE_BACKEND',hostname]))
        if res == 'TRUE':
            return True
        else:
            return False

    @FileOps._ProgramQuery('QUERY_RECORDINGS Ascending', sorted=True)
    def getRecordings(self, pg):
        """
        Returns a list of all Program objects which have already recorded
        """
        return pg

    @FileOps._ProgramQuery('QUERY_GETEXPIRING', sorted=True)
    def getExpiring(self, pg):
        """
        Returns a tuple of all Program objects nearing expiration
        """
        return pg

    def getCheckfile(self,program):
        """
        Returns location of recording in file system
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                        ['QUERY_CHECKFILE','1',program.toString()]\
                    )).split(BACKEND_SEP)
        if res[0] == 0:
            return None
        else:
            return res[1]

    def getFreeSpace(self,all=False):
        """
        Returns a tuple of FreeSpace objects
        """
        command = 'QUERY_FREE_SPACE'
        if all:
            command = 'QUERY_FREE_SPACE_LIST'
        res = self.backendCommand(command).split(BACKEND_SEP)
        l = len(FreeSpace._field_order)
        for i in range(len(res)//l):
            yield FreeSpace(res[i*l:i*l+l])

    def getFreeSpaceSummary(self):
        """
        Returns a tuple of total space (in KB) and used space (in KB)
        """
        res = [int(r) for r in self.backendCommand('QUERY_FREE_SPACE_SUMMARY')\
                                   .split(BACKEND_SEP)]
        return (res[0], res[1])

    def getLoad(self):
        """
        Returns a tuple of the 1, 5, and 15 minute load averages
        """
        return [float(r) for r in self.backendCommand('QUERY_LOAD')\
                                      .split(BACKEND_SEP)]

    def getUptime(self):
        """
        Returns machine uptime in seconds
        """
        return timedelta(0, int(self.backendCommand('QUERY_UPTIME')))

    def walkSG(self, host, sg, top=None):
        """
        Performs similar to os.walk(), returning a tuple of tuples containing
            (dirpath, dirnames, filenames).
        'dirnames' is a tuple of all directories at the current level.
        'filenames' is a dictionary, where the values are the file sizes.
        """
        def walk(self, host, sg, root, path):
            res = self.getSGList(host, sg, root+path+'/')
            if isinstance(res, tuple):
                dlist = list(res[0])
                res = [dlist, dict(zip(res[1], res[2]))]
                if path == '':
                    res = {'/':res}
                else:
                    res = {path:res}
                for d in dlist:
                    res.update(walk(self, host, sg, root, path+'/'+d))
                return res
            else:
                return {}

        bases = self.getSGList(host, sg, '')
        if (top == '/') or (top is None): top = ''
        walked = {}
        for base in bases:
            res = walk(self, host, sg, base, top)
            for d in res:
                if d in walked:
                    walked[d][0] += res[d][0]
                    walked[d][1].update(res[d][1])
                else:
                    walked[d] = res[d]

        res = []
        for key, val in list(walked.items()):
            res.append((key, tuple(val[0]), val[1]))
        res.sort()
        return res

    def getSGList(self,host,sg,path,filenamesonly=False):
        """
        Returns a tuple of directories, files, and filesizes.
        Two special modes
            'filenamesonly' returns only filenames, no directories or size
            empty path base directories returns base storage group paths
        """
        if filenamesonly: path = '' # force empty path
        res = self.backendCommand(BACKEND_SEP.join(\
                    ['QUERY_SG_GETFILELIST',host,sg,path,
                    str(int(filenamesonly))]\
                )).split(BACKEND_SEP)
        if res[0] == 'EMPTY LIST':
            return -1
        if res[0] == 'SLAVE UNREACHABLE: ':
            return -2
        dirs = []
        files = []
        sizes = []
        for entry in res:
            if filenamesonly:
                files.append(entry)
            elif path == '':
                type,name = entry.split('::')
                dirs.append(name)
            else:
                se = entry.split('::')
                if se[0] == 'file':
                    files.append(se[1])
                    sizes.append(se[2])
                elif se[0] == 'dir':
                    dirs.append(se[1])
        if filenamesonly:
            return files
        elif path == '':
            return dirs
        else:
            return (dirs, files, sizes)

    def getSGFile(self,host,sg,path):
        """
        Returns a tuple of last modification time and file size
        """
        res = self.backendCommand(BACKEND_SEP.join(\
                ['QUERY_SG_FILEQUERY',host,sg,path])).split(BACKEND_SEP)
        if res[0] == 'EMPTY LIST':
            return -1
        if res[0] == 'SLAVE UNREACHABLE: ':
            return -2
        return tuple(res[1:3])

    def getLastGuideData(self):
        """
        Returns the last dat for which guide data is available
        """
        try:
            return datetime.duck(self.backendCommand('QUERY_GUIDEDATATHROUGH'))
        except ValueError:
            return None

    def clearSettings(self):
        """
        Triggers an event to clear the settings cache on all active systems.
        """
        self.backendCommand('MESSAGE%sCLEAR_SETTINGS_CACHE' % BACKEND_SEP)
        self.db.settings.clear()

class BEEventMonitor( BECache ):
    def __init__(self, backend=None, blockshutdown=False,
                       systemevents=False, db=None):
        self.systemevents = systemevents
        super(BEEventMonitor, self).__init__(backend, blockshutdown, True, db)

    def _listhandlers(self):
        return [self.eventMonitor]

    def _neweventconn(self):
        return BEEventConnection(self.host, self.port, self.db.gethostname(),
                    level = (2,1)[self.systemevents])

    def eventMonitor(self, event=None):
        if event is None:
            return re.compile('BACKEND_MESSAGE')
        self.log(MythLog.ALL, MythLog.INFO, event)

class MythSystemEvent( BECache ):
    class systemeventhandler( object ):
        # decorator class for system events
        bs = BACKEND_SEP.replace('[',r'\[').replace(']',r'\]')
        re_process = re.compile(bs.join([
                r'BACKEND_MESSAGE',
                r'SYSTEM_EVENT (?P<event>[A-Z0-9_]*)'
                    r'( HOSTNAME (?P<hostname>[a-zA-Z0-9_\.]*))?'
                    r'( SENDER (?P<sender>[a-zA-Z0-9_\.]*))?'
                    r'( CARDID (?P<cardid>[0-9]*))?'
                    r'( CHANID (?P<chanid>[0-9]*))?'
                    r'( STARTTIME (?P<starttime>[0-9-]*T[0-9-]))?'
                    r'( SECS (?P<secs>[0-9]*))?',
                r'empty']))

        def __init__(self, func):
            self.func = func
            self.__doc__ = self.func.__doc__
            self.__name__ = self.func.__name__
            self.__module__ = self.func.__module__
            self.filter = None

            self.re_search = re.compile('BACKEND_MESSAGE%sSYSTEM_EVENT %s' %\
                                (self.bs, self.__name__.upper()))

            self.generic = (self.__name__ == '_generic_handler')
            if self.generic:
                self.re_search = re.compile(\
                                'BACKEND_MESSAGE%sSYSTEM_EVENT' % self.bs)

        def __get__(self, inst, own):
            self.inst = inst
            return self

        def __call__(self, event=None):
            if event is None:
                return self.re_search

            # basic string processing
            match = self.re_process.match(event)
            event = {}
            for a in ('event','hostname','sender','cardid','secs'):
                if match.group(a) is not None:
                    event[a] = match.group(a)

            # event filter for generic handler
            if self.generic:
                if self.filter is None:
                    self.filter = [f.__name__.upper() \
                                        for f in self.inst._events]
                if event['event'] in self.filter:
                    return

            # pull program information
            if (match.group('chanid') is not None) and \
               (match.group('starttime') is not None):
                    be = MythBE(self.inst.hostname)
                    event['program'] = be.getRecording(\
                          (match.group('chanid'),match.group('starttime')))

            self.func(self.inst, event)

    def _listhandlers(self):
        return []

    def __init__(self, backend=None, blockshutdown=False, db=None,
                       enablehandler=True):
        super(MythSystemEvent, self).__init__(backend, blockshutdown, True, db)

        if enablehandler:
            self._events.append(self._generic_handler)
            self.registerevent(self._generic_handler)

    def _neweventconn(self):
        return BEEventConnection(self.host, self.port, self.db.gethostname(), level=3)

    @systemeventhandler
    def _generic_handler(self, event):
        SystemEvent(event['event'], self.db).command(event)

class Frontend( FEConnection ):
    _db = None
    class _Jump( object ):
        def __str__(self):  return str(self.list())
        def __repr__(self): return str(self)

        def __init__(self, parent):
            self._parent = proxy(parent)
            self._points = {}

        def _populate(self):
            self._populate = _donothing
            self._points = dict(self._parent.send('jump'))

        def __getitem__(self, key):
            self._populate()
            if key in self._points:
                return self._parent.send('jump', key)
            else:
                return False

        def __getattr__(self, key):
            self._populate()
            if key in self.__dict__:
                return self.__dict__[key]
            return self.__getitem__(key)

        def dict(self):
            self._populate()
            return self._points

        def list(self):
            self._populate()
            return self._points.items()

    class _Key( object ):
        _keymap = { 9:'tab',        10:'enter',     27:'escape',
                    32:'space',     92:'backslash', 127:'backspace',
                    258:'down',     259:'up',       260:'left',
                    261:'right',    265:'f1',       266:'f2',
                    267:'f3',       268:'f4',       269:'f5',
                    270:'f6',       271:'f7',       272:'f8',
                    273:'f9',       274:'f10',      275:'f11',
                    276:'f12',      330:'delete',   331:'insert',
                    338:'pagedown', 339:'pageup'}
        _alnum = [chr(i) for i in list(range(48,58))+list(range(65,91))+list(range(97,123))]

        def __str__(self):  return str(self.list())
        def __repr__(self): return str(self)

        def __init__(self, parent):
            self._parent = proxy(parent)
            self._keys = []

        def _populate(self):
            self._populate = _donothing
            self._keys = self._parent.send('key')

        def _sendLiteral(self, key):
            if (key in self._keys) or (key in self._alnum):
                return self._parent.send('key', key)
            return False

        def _sendOrdinal(self, key):
            try:
                key = int(key)
                if key in self._keymap:
                    key = self._keymap[key]
                else:
                    key = chr(key)
                return self._sendLiteral(key)
            except ValueError:
                return False

        def __getitem__(self, key):
            self._populate()
            if self._sendOrdinal(key):
                return True
            elif self._sendLiteral(key):
                return True
            else:
                return False

        def __getattr__(self, key):
            self._populate()
            if key in self.__dict__:
                return self.__dict__[key]
            return self._sendLiteral(key)

        def list(self):
            self._populate()
            return self._keys

    def __init__(self, *args, **kwargs):
        FEConnection.__init__(self, *args, **kwargs)
        self.jump = self._Jump(self)
        self.key = self._Key(self)

    def sendQuery(self,query): return self.send('query', query)
    def getQuery(self): return self.send('query')
    def sendPlay(self,play): return self.send('play', play)
    def getPlay(self): return self.send('play')

    def play(self, media):
        """Plays selected media on frontend."""
        return media._playOnFe(self)

    def getLoad(self):
        """Returns tuple of 1/5/15 load averages."""
        return tuple([float(l) for l in self.sendQuery('load').split()])

    def getUptime(self):
        """Returns timedelta of uptime."""
        return timedelta(0, int(self.sendQuery('uptime')))

    def getTime(self):
        """Returns current time on frontend."""
        return datetime.fromIso(self.sendQuery('time'))

    def getMemory(self):
        """Returns free and total, physical and swap memory."""
        return dict(zip(('totalmem','freemem','totalswap','freeswap'),
                    [int(m) for m in self.sendQuery('memstats').split()]))

    def getScreenShot(self):
        """Returns PNG image of the frontend."""
        fd = urlopen('http://%s:6547/MythFE/GetScreenShot' % self.host)
        img = fd.read()
        fd.close()
        return img

class MythDB( DBCache ):
    __doc__ = DBCache.__doc__+"""
        obj.searchRecorded()    - return a list of matching Recorded objects
        obj.searchOldRecorded() - return a list of matching
                                  OldRecorded objects
        obj.searchJobs()        - return a list of matching Job objects
        obj.searchGuide()       - return a list of matching Guide objects
        obj.searchRecord()      - return a list of matching Record rules
        obj.getFrontends()      - return a list of available Frontends
        obj.getFrontend()       - return a single Frontend object
    """


    @databaseSearch
    def searchRecorded(self, init=False, key=None, value=None):
        """
        obj.searchRecorded(**kwargs) -> list of Recorded objects

        Supports the following keywords:
            title,      subtitle,   chanid,     starttime,  progstart,
            category,   hostname,   autoexpire, commflagged,
            stars,      recgroup,   playgroup,  duplicate,  transcoded,
            watched,    storagegroup,           category_type,
            airdate,    stereo,     subtitled,  hdtv,       closecaptioned,
            partnumber, parttotal,  seriesid,   showtype,   programid,
            manualid,   generic,    cast,       livetv,     basename,
            syndicatedepisodenumber,            olderthan,  newerthan,
            inetref,    season,     episode,    recordedid

        Multiple keywords can be chained as such:
            obj.searchRecorded(title='Title', commflagged=False)
        """

        if init:
            # table and join descriptor
            init.table = 'recorded'
            init.handler = Recorded
            init.require = ('livetv',)
            init.joins = (init.Join(table='recordedprogram',
                                    tableto='recorded',
                                    fieldsfrom=('chanid','starttime'),
                                    fieldsto=('chanid','progstart')),
                          init.Join(table='recordedcredits',
                                    tableto='recorded',
                                    fieldsfrom=('chanid','starttime'),
                                    fieldsto=('chanid','progstart')),
                          init.Join(table='people',
                                    tableto='recordedcredits',
                                    fields=('person',)))
            return None

        # local table matches
        if key in ('title','subtitle','chanid',
                        'category','hostname','autoexpire','commflagged',
                        'stars','recgroup','playgroup','duplicate',
                        'transcoded','watched','storagegroup','basename',
                        'inetref','season','episode', 'recordedid'):
            return ('recorded.%s=%%s' % key, value, 0)

        # time matches
        if key in ('starttime','endtime','progstart','progend'):
            return ('recorded.%s=?' % key, datetime.duck(value), 0)

        if key == 'olderthan':
            return ('recorded.starttime<?', datetime.duck(value), 0)
        if key == 'newerthan':
            return ('recorded.starttime>?', datetime.duck(value), 0)

        # recordedprogram matches
        if key in ('category_type','airdate','stereo','subtitled','hdtv',
                    'closecaptioned','partnumber','parttotal','seriesid',
                    'showtype','syndicatedepisodenumber','programid',
                    'manualid','generic'):
            return ('recordedprogram.%s=?' % key, value, 1)

        if key == 'cast':
            return ('people.name', 'recordedcredits', 4, 1)

        if key == 'livetv':
            if (value is None) or (value == False):
                return ('recorded.recgroup!=?', 'LiveTV', 0)
            return ()

        return None

    @databaseSearch
    def searchOldRecorded(self, init=False, key=None, value=None):
        """
        obj.searchOldRecorded(**kwargs) -> list of OldRecorded objects

        Supports the following keywords:
            title,      subtitle,   chanid,     starttime,  endtime,
            category,   seriesid,   programid,  station,    duplicate,
            generic,    recstatus,  inetref,    season,     episode
        """

        if init:
            init.table = 'oldrecorded'
            init.handler = OldRecorded
            return None

        if key in ('title','subtitle','chanid',
                        'category','seriesid','programid','station',
                        'duplicate','generic','recstatus','inetref',
                        'season','episode'):
            return ('oldrecorded.%s=?' % key, value, 0)
                # time matches
        if key in ('starttime','endtime'):
            return ('oldrecorded.%s=?' % key, datetime.duck(value), 0)
        return None

    @databaseSearch
    def searchArtwork(self, init=False, key=None, value=None):
        """
        obj.searchArtwork(**kwargs) -> list of RecordedArtwork objects

        Supports the following keywords:
            inetref,    season,     host,   chanid,     starttime
            title,      subtitle
        """

        if init:
            # table and join descriptor
            init.table = 'recordedartwork'
            init.handler = RecordedArtwork
            init.joins = (init.Join(table='recorded',
                                    tableto='recordedartwork',
                                    fieldsfrom=('inetref','season','hostname'),
                                    fieldsto=('inetref','season','host')),)
            return None

        if key in ('inetref', 'season', 'host'):
            return ('recordedartwork.%s=?' % key, value, 0)

        if key in ('chanid', 'title', 'subtitle'):
            return ('recorded.%s=?' % key, value, 1)

        if key == 'starttime':
            return ('recorded.%s=?' % key, datetime.duck(value), 1)

        return None

    @databaseSearch
    def searchJobs(self, init=False, key=None, value=None):
        """
        obj.searchJobs(**kwars) -> list of Job objects

        Supports the following keywords:
            chanid,     starttime,  type,       status,     hostname,
            title,      subtitle,   flags,      olderthan,  newerthan
        """
        if init:
            init.table = 'jobqueue'
            init.handler = Job
            init.joins = (init.Join(table='recorded',
                                    tableto='jobqueue',
                                    fields=('chanid','starttime')),)
            return None

        if key in ('chanid','type','status','hostname'):
            return ('jobqueue.%s=?' % key, value, 0)
        if key in ('title','subtitle'):
            return ('recorded.%s=?' % key, value, 1)
        if key == 'flags':
            return ('jobqueue.flags&?', value, 0)

        if key == 'starttime':
            return ('jobqueue.starttime=?', datetime.duck(value), 0)
        if key == 'olderthan':
            return ('jobqueue.inserttime>?', datetime.duck(value), 0)
        if key == 'newerthan':
            return ('jobqueue.inserttime<?', datetime.duck(value), 0)
        return None

    @databaseSearch
    def searchGuide(self, init=False, key=None, value=None):
        """
        obj.searchGuide(**args) -> list of Guide objects

        Supports the following keywords:
            chanid,     starttime,  endtime,    title,      subtitle,
            category,   airdate,    stars,      previouslyshown,
            stereo,     subtitled,  hdtv,       closecaptioned,
            partnumber, parttotal,  seriesid,   originalairdate,
            showtype,   programid,  generic,    syndicatedepisodenumber,
            ondate,     cast,       startbefore,startafter
            endbefore,  endafter,   dayofweek,  weekday
            first,      last,       callsign,   commfree
            channelgroup,           videosource,
            genre,      rating,     cast,       fuzzytitle
            fuzzysubtitle,          fuzzydescription,
            fuzzyprogramid,         beforedate, afterdate,
        """
        if init:
            init.table = 'program'
            init.handler = Guide
            init.joins = (init.Join(table='credits',
                                    tableto='program',
                                    fields=('chanid','starttime')),
                          init.Join(table='people',
                                    tableto='credits',
                                    fields=('person',)),
                          init.Join(table='channel',
                                    tableto='program',
                                    fields=('chanid',)),
                          init.Join(table='channelgroup',
                                    tableto='program',
                                    fields=('chanid',)),
                          init.Join(table='channelgroupnames',
                                    tableto='channelgroup',
                                    fields=('grpid',)),
                          init.Join(table='videosource',
                                    tableto='channel',
                                    fields=('sourceid',)),
                          init.Join(table='programgenres',
                                    tableto='program',
                                    fields=('chanid','starttime')),
                          init.Join(table='programrating',
                                    tableto='program',
                                    fields=('chanid','starttime')))
            return None

        if key in ('chanid','title','subtitle',
                        'category','airdate','stars','previouslyshown','stereo',
                        'subtitled','hdtv','closecaptioned','partnumber',
                        'parttotal','seriesid','originalairdate','showtype',
                        'syndicatedepisodenumber','programid','generic',
                        'category_type'):
            return ('program.%s=?' % key, value, 0)
        if key in ('starttime','endtime'):
            return ('program.%s=?' % key, datetime.duck(value), 0)
        if key == 'dayofweek':
            return ('DAYNAME(program.starttime)=?', value, 0)
        if key == 'weekday':
            return ('WEEKDAY(program.starttime)<?', 5, 0)
        if key in ('first', 'last'):
            return ('program.%s=?' % key, 1, 0)

        if key == 'callsign':
            return ('channel.callsign=?', value, 4)
        if key == 'commfree':
            return ('channel.commmethod=?', -2, 4)
        if key == 'channelgroup':
            return ('channelgroupnames.name=?', value, 24)
        if key == 'videosource':
            try:
                value = int(value)
                return ('channel.sourceid=?', value, 4)
            except:
                return ('videosource.name=?', value, 36)
        if key == 'genre':
            return ('programgenres.genre=?', value, 64)
        if key == 'rating':
            return ('programrating.rating=?', value, 128)
        if key == 'cast':
            return ('people.name', 'credits', 2, 0)

        if key.startswith('fuzzy'):
            if key[5:] in ('title', 'subtitle', 'description', 'programid'):
                return ('program.%s LIKE ?' % key[5:], '%'+value+'%', 0)
            if key[5:] == 'callsign':
                return ('channel.callsign LIKE ?', '%'+value+'%', 4)
        if key.endswith('date'):
            prefix = {'on':'=', 'before':'<', 'after':'>'}
            if key[:-4] in prefix:
                return ('DATE(program.starttime){0}?'.format(prefix[key[:-4]]),
                        value, 0)

        if key == 'startbefore':
            return ('program.starttime<?', datetime.duck(value), 0)
        if key == 'startafter':
            return ('program.starttime>?', datetime.duck(value), 0)
        if key == 'endbefore':
            return ('program.endtime<?', datetime.duck(value), 0)
        if key == 'endafter':
            return ('program.endtime>?', datetime.duck(value), 0)
        return None

    def makePowerRule(self, ruletitle='unnamed (Power Search',
                            type=RECTYPE.kAllRecord, **kwargs):
        where, args, join = self.searchGuide.parseInp(kwargs)
        where = ' AND '.join(where)
        join = self.searchGuide.buildJoin(join)
        return Record.fromPowerRule(title=ruletitle, type=type, where=where,
                                    args=args, join=join, db=self)

    @databaseSearch
    def searchRecord(self, init=False, key=None, value=None):
        """
        obj.searchRecord(**kwargs) -> list of Record objects

        Supports the following keywords:
            type,       chanid,     starttime,  startdate,  endtime
            enddate,    title,      subtitle,   category,   profile
            recgroup,   station,    seriesid,   programid,  playgroup,
            inetref
        """
        if init:
            init.table = 'record'
            init.handler = Record
            return None

        if key in ('type','chanid','starttime','startdate','endtime','enddate',
                        'title','subtitle','category','profile','recgroup',
                        'station','seriesid','programid','playgroup','inetref'):
            return ('%s=?' % key, value, 0)
        return None

    @databaseSearch
    def searchInternetContent(self, init=False, key=None, value=None):
        """
        obj.searchInternetContent(**kwargs) -> list of content

        Supports the following keywords:
            feedtitle,  title,      subtitle,   season,     episode,
            url,        type,       author,     rating,     player,
            width,      height,     language,   podcast,    downloadable,
            ondate,     olderthan,  newerthan,  longerthan, shorterthan,
            country,    description
        """
        if init:
            init.table = 'internetcontentarticles'
            init.handler = InternetContentArticles
            return None

        if key in ('feedtitle','title','subtitle','season','episode','url',
                        'type','author','rating','player','width','height',
                        'language','podcast','downloadable', 'description'):
            return ('%s=?' % key, value, 0)
        if key == 'ondate':
            return ('DATE(date)=?', value, 0)
        if key == 'olderthan':
            return ('date>?', value, 0)
        if key == 'newerthan':
            return ('date<?', value, 0)
        if key == 'longerthan':
            return ('time<?', value, 0)
        if key == 'shorterthan':
            return ('time>?', value, 0)
        if key == 'country':
            return ('countries LIKE ?', '%'+value+'%', 0)

    def getFrontends(self):
        """
        Returns a list of Frontend objects for accessible frontends
        """
        with self.cursor(self.log) as cursor:
            cursor.execute("""SELECT DISTINCT hostname
                              FROM settings
                              WHERE hostname IS NOT NULL
                              AND value='NetworkControlEnabled'
                              AND data=1""")
            frontends = [(fe[0], self.settings[fe[0]].NetworkControlPort) \
                            for fe in cursor]
        return Frontend.testList(frontends)

    def getFrontend(self,host):
        """
        Returns a Frontend object for the specified host
        """
        port = self.settings[host].NetworkControlPort
        return Frontend(host,port)

    def scanVideos(self):
        """
        obj.scanVideo() --> list of new, moved, and deleted Videos
        """
        startvids = dict([(vid.intid, vid) for vid in Video.getAllEntries(db=self)])

        be = MythBE(db=self)
        r = re.compile(re.escape(BACKEND_SEP).\
                join(['BACKEND_MESSAGE',
                      '(VIDEO_LIST(_NO)?_CHANGE)',
                      '(.*)']))
        lock = EventLock(r, db=self)

        be.backendCommand("SCAN_VIDEOS")
        lock.wait()

        newvids = []
        movvids = []
        oldvids = []

        match = r.match(lock.event)
        if match.group(1) == "VIDEO_LIST_CHANGE":
            for entry in match.group(3).split(BACKEND_SEP):
                type,intid = entry.split('::')
                intid = int(intid)
                if type == 'added':
                    newvids.append(Video(intid))
                elif type == 'moved':
                    v = startvids[intid]
                    v._pull()
                    movvids.append(v)
                elif type == 'deleted':
                    oldvids.append(startvids[intid])

        return newvids,movvids,oldvids

    @databaseSearch
    def searchVideos(self, init=False, key=None, value=None):
        """
        obj.searchVideos(**kwargs) -> list of Video objects

        Supports the following keywords:
            title, subtitle, season, episode, host, director, year, cast,
            genre, country, category, insertedbefore, insertedafter, custom
        """
        if init:
            init.table = 'videometadata'
            init.handler = Video
            init.joins = (init.Join(table='videometadatacast',
                                    tableto='videometadata',
                                    fieldsfrom=('idvideo',),
                                    fieldsto=('intid',)),
                          init.Join(table='videocast',
                                    tableto='videometadatacast',
                                    fieldsfrom=('intid',),
                                    fieldsto=('idcast',)),
                          init.Join(table='videometadatagenre',
                                    tableto='videometadata',
                                    fieldsfrom=('idvideo',),
                                    fieldsto=('intid',)),
                          init.Join(table='videogenre',
                                    tableto='videometadatagenre',
                                    fieldsfrom=('intid',),
                                    fieldsto=('idgenre',)),
                          init.Join(table='videometadatacountry',
                                    tableto='videometadata',
                                    fieldsfrom=('idvideo',),
                                    fieldsto=('intid',)),
                          init.Join(table='videocountry',
                                    tableto='videometadatacountry',
                                    fieldsfrom=('intid',),
                                    fieldsto=('idcountry',)),
                          init.Join(table='videocategory',
                                    tableto='videometadata',
                                    fieldsfrom=('intid',),
                                    fieldsto=('category',)))
            return None

        if key in ('title','subtitle','season','episode','host',
                        'director','year'):
            return('videometadata.%s=?' % key, value, 0)
        vidref = {'cast':(2,0), 'genre':(8,2), 'country':(32,4)}
        if key in vidref:
            return ('video%s.%s' % (key,key),
                    'videometadata%s' % key,
                    vidref[key][0],
                    vidref[key][1])
        if key == 'category':
            return ('videocategory.%s=?' % key, value, 64)
        if key == 'exactfile':
            return ('videometadata.filename=?', value, 0)
        if key == 'file':
            return ('videometadata.filename LIKE ?', '%'+value, 0)
        if key == 'insertedbefore':
            return ('videometadata.insertdate<?', value, 0)
        if key == 'insertedafter':
            return ('videometadata.insertdate>?', value, 0)
        return None

class MythVideo( MythDB ):
    """legacy - do not use"""
    def scanStorageGroups(self, delete=True):
        """legacy - do not use"""
        added, moved, deleted = scanVideos()
        return (added, deleted)

class MythXML( XMLConnection ):
    """
    Provides convenient methods to access the backend XML server.
    Parameter 'backend' is either a hostname from 'settings',
    an ip address or a hostname in ip-notation.
    """
    def __init__(self, backend=None, port=None, db=None):
        if backend and port:
            XMLConnection.__init__(self, backend, port)
            self.db = db
            return

        self.db = DBCache(db)
        self.log = MythLog('Python XML Connection')
        if backend is None:
            # use master backend
            backend = self.db.getMasterBackend()

        # assume hostname from settings
        host = self.db._getpreferredaddr(backend)
        if host:
            port = int(self.db.settings[backend].BackendStatusPort)
        else:
            # assume ip address
            hostname = self.db._gethostfromaddr(backend)
            host = backend
            port = int(self.db.settings[hostname].BackendStatusPort)

        # resolve ip address from name
        reshost, resport = resolve_ip(host,port)
        if not reshost:
            raise MythDBError(MythError.DB_SETTING,
                                backend+': BackendServerAddr')
        if not resport:
            raise MythDBError(MythError.DB_SETTING,
                                backend+': BackendStatusPort')
        self.host = host
        self.port = port

    def getHosts(self):
        """Returns a list of unique hostnames found in the settings table."""
        return self._request('Myth/GetHosts')\
                                        .readJSON()['StringList']

    def getKeys(self):
        """Returns a list of unique keys found in the settings table."""
        return self._request('Myth/GetKeys')\
                                        .readJSON()['StringList']

    def getSetting(self, key, hostname=None, default=None):
        """Retrieves a setting from the backend."""
        args = {'Key':key}
        if hostname:
            args['HostName'] = hostname
        if default:
            args['Default'] = default
        return self._request('Myth/GetSetting', **args)\
                                        .readJSON()['String']

    def getProgramGuide(self, starttime, endtime, startchan, numchan=None):
        """
        Returns a list of Guide objects corresponding to the given time period.
        """
        starttime = datetime.duck(starttime)
        endtime = datetime.duck(endtime)
        args = {'StartTime':starttime.utcisoformat().rsplit('.',1)[0],
                'EndTime':endtime.utcisoformat().rsplit('.',1)[0],
                'StartChanId':startchan, 'Details':'1'}
        if numchan:
            args['NumOfChannels'] = numchan
        else:
            args['NumOfChannels'] = '1'

        dat = self._request('Guide/GetProgramGuide', **args).readJSON()
        for chan in dat['ProgramGuide']['Channels']:
            for prog in chan['Programs']:
                prog['ChanId'] = chan['ChanId']
                yield Guide.fromJSON(prog, self.db)

    def getProgramDetails(self, chanid, starttime):
        """
        Returns a Program object for the matching show.
        """
        starttime = datetime.duck(starttime)
        args = {'ChanId': chanid, 'StartTime': starttime.utcisoformat()}
        return Program.fromJSON(
                self._request('Guide/GetProgramDetails', **args)\
                    .readJSON()['Program'],
                db=self.db)

    def getChannelIcon(self, chanid):
        """Returns channel icon as a data string"""
        return self._request('Guide/GetChannelIcon', ChanId=chanid).read()

    def getRecorded(self, descending=True):
        """
        Returns a list of Program objects for recorded shows on the backend.
        """
        descendingstr = 'true' if descending else 'false'
        for prog in self._request('Dvr/GetRecordedList', Descending=descendingstr)\
                    .readJSON()['ProgramList']['Programs']:
            yield Program.fromJSON(prog, self.db)

    def getExpiring(self):
        """
        Returns a list of Program objects for expiring shows on the backend.
        """
        for prog in self._request('Dvr/GetExpiringList')\
                    .readJSON()['ProgramList']['Programs']:
            yield Program.fromJSON(prog, self.db)

#    def getInternetSources(self):
#        for grabber in self._queryTree('GetInternetSources').\
#                        find('InternetContent').findall('grabber'):
#            yield InternetSource.fromEtree(grabber, self)

    def getInternetContentUrl(self, grabber, videocode):
        return "mythflash://%s:%s/InternetContent/GetInternetContent?Grabber=%s&videocode=%s" \
            % (self.host, self.port, grabber, videocode)

    def getPreviewImage(self, chanid, starttime, width=None, \
                                                 height=None, secsin=None):
        starttime = datetime.duck(starttime)
        args = {'ChanId':chanid, 'StartTime':starttime.utcisoformat()}
        if width: args['Width'] = width
        if height: args['Height'] = height
        if secsin: args['SecsIn'] = secsin

        return self._request('Content/GetPreviewImage', **args).read()

class MythMusic( MusicSchema, DBCache ):
    """
    Provides convenient methods to access the MythTV MythMusic database.
    """
    @databaseSearch
    def searchMusic(self, init=False, key=None, value=None):
        """
        obj.searchMusic(**kwargs) -> iterable of Song objects

        Supports the following keywords:
            name,  track,  disc_number, artist,      album,   year
            genre, rating, format,      sample_rate, bitrate
        """
        if init:
            init.table = 'music_songs'
            init.handler = Song
            init.joins = (init.Join(table='music_artists',
                                    tableto='music_songs',
                                    fields=('artist_id',)),
                          init.Join(table='music_albums',
                                    tableto='music_songs',
                                    fields=('album_id',)),
                          init.Join(table='music_genres',
                                    tableto='music_songs',
                                    fields=('genre_id',)))

        if key in ('name','track','disc_number','rating',
                        'format','sample_rate','bitrate', 'hostname'):
            return ('music_songs.%s=%%s' % key, value, 0)
        if key == 'artist':
            return ('music_artists.artist_name=%s', value, 1)
        if key == 'album':
            return ('music_albums.album_name=%s', value, 2)
        if key == 'year':
            return ('music_albums.year=%s', value, 2)
        if key == 'genre':
            return ('music_genres.genre=%s', value, 4)
