#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Provides classes for connecting to a MythTV backend.

The MythTV class to handle connection to and querying of a MythTV backend.
The Recorder class representing and storing details of a tuner card.
The Program class for storing details of a TV program.
"""
import os
import sys
import socket
import shlex
import socket
import code
import re
from datetime import datetime
from time import mktime, strftime

from MythDB import *
from MythLog import *

log = MythLog(CRITICAL, '#%(levelname)s - %(message)s', 'MythTV')

RECSTATUS = {
        'TunerBusy': -8,
        'LowDiskSpace': -7,
        'Cancelled': -6,
        'Deleted': -5,
        'Aborted': -4,
        'Recorded': -3,
        'Recording': -2,
        'WillRecord': -1,
        'Unknown': 0,
        'DontRecord': 1,
        'PreviousRecording': 2,
        'CurrentRecording': 3,
        'EarlierShowing': 4,
        'TooManyRecordings': 5,
        'NotListed': 6,
        'Conflict': 7,
        'LaterShowing': 8,
        'Repeat': 9,
        'Inactive': 10,
        'NeverRecord': 11,
        }

BACKEND_SEP = '[]:[]'
PROTO_VERSION = 50
PROGRAM_FIELDS = 47

class Backend(object):
    """
    Basic class for mythbackend socket connections
    """

    db = None

    def __init__(self, backend=None, type='Monitor'):
        if self.db is None:
            self.db = MythDB(sys.argv[1:])
        if backend is None:
            # use master backend
            self.host = self.db.getSetting('MasterServerIP')
            self.port = self.db.getSetting('MasterServerPort')
        elif re.match('(?:\d{1,3}\.){3}\d{1,3}',backend):
            # process ip address
            self.host = backend
            c = self.db.cursor()
            if c.execute("""SELECT hostname FROM settings WHERE
                    value='BackendServerIP' AND
                    data=%s""", self.host) == 0:
                backend = None
            else:
                backend = c.fetchone()[0]
            self.port = self.db.getSetting('BackendServerPort',backend)
            c.close()
        else:
            # assume given a hostname
            self.host = self.db.getSetting('BackendServerIP',backend)
            if not self.host:
                # try a truncated hostname
                backend = backend.split('.')[0]
                self.host = self.db.getSetting('BackendServerIP',backend)
                if not self.host:
                    raise MythError('Unable to find requested backend.')
            self.port = self.db.getSetting('BackendServerPort',backend)

        if not self.host:
            log.Msg(CRITICAL, 'Unable to find requested backend '+
                                'IP in database')
            raise MythError('Unable to find requested backend IP in database')
        if not self.port:
            log.Msg(CRITICAL, 'Unable to find requested backend '+
                                'port in database')
            raise MythError('Unable to find requested backend '+
                            'port in database')
        self.port = int(self.port)

        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(10)
            self.socket.connect((self.host, self.port))
            self.check_version()
            self.announce(type)
        except socket.error, e:
            log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d '+
                        '(is the backend running)', self.host, self.port)
            raise MythError("Couldn't connect to %s:%d "+
                        "(is the backend running)" % (self.host,self.port))
        except:
            raise

    def __del__(self):
        self.backendCommand('DONE')
        self.socket.shutdown(1)
        self.socket.close()

    def close(self): self.__del__()

    def check_version(self):
        res = self.backendCommand('MYTH_PROTO_VERSION %s' \
                    % PROTO_VERSION).split(BACKEND_SEP)
        if res[0] == 'REJECT':
            log.Msg(CRITICAL, 'Backend has version %s '+
                    'and we speak version %s', res[1], PROTO_VERSION)
            raise MythError('Backend has version %s '+
                    'and we speak version %s' % (res[1], PROTO_VERSION))

    def announce(self,type):
        res = self.backendCommand('ANN %s %s 0' % (type, socket.gethostname()))
        if res != 'OK':
            log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
            raise MythError('Unexpected answer to ANN command: %s' % res)
        else:
            log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', 
                            self.host, self.port)

    def backendCommand(self, data):
        """
        Sends a formatted command via a socket to the mythbackend.

        Returns the result from the backend.
        """
        def recv():
            """
            Reads the data returned from the backend.
            """
            # The first 8 bytes of the response gives us the length
            data = self.socket.recv(8)
            try:
                length = int(data)
            except:
                return u''
            data = []
            while length > 0:
                chunk = self.socket.recv(length)
                length = length - len(chunk)
                data.append(chunk)
            try:
                return unicode(''.join(data),'utf8')
            except:
                return u''.join(data)

        command = u'%-8d%s' % (len(data), data)
        log.Msg(DEBUG, 'Sending command: %s', command)
        self.socket.send(command)
        return recv()

    def joinInt(self,high,low):
        """
        Returns a single long from a pair of signed integers
        """
        return (high + (low<0))*2**32 + low

    def splitInt(self,integer):
        """
        Returns a pair of signed integers from a single long
        """
        return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32


class MythTV(Backend):
    """
    A connection to a MythTV backend.
    """

    locked_tuners = []

    def __del__(self):
        self.freeTuner()
        Backend.__del__(self)

    def getPendingRecordings(self):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        programs = []
        res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
        has_conflict = int(res.pop(0))
        num_progs = int(res.pop(0))
        log.Msg(DEBUG, '%s pending recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getScheduledRecordings(self):
        """
        Returns a list of Program objects which are scheduled to be recorded.
        """
        programs = []
        res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        log.Msg(DEBUG, '%s scheduled recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getUpcomingRecordings(self):
        """
        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.
        """
        def sort_programs_by_starttime(x, y):
            if x.starttime > y.starttime:
                return 1
            elif x.starttime == y.starttime:
                return 0
            else:
                return -1
        programs = []
        res = self.getPendingRecordings()
        for p in res:
            if p.recstatus == RECSTATUS['WillRecord']:
                programs.append(p)
        programs.sort(sort_programs_by_starttime)
        return tuple(programs)

    def getRecorderList(self):
        """
        Returns a list of recorders, or an empty list if none.
        """
        recorders = []
        c = self.db.cursor()
        c.execute('SELECT cardid FROM capturecard')
        row = c.fetchone()
        while row is not None:
            recorders.append(int(row[0]))
            row = c.fetchone()
        c.close()
        return recorders

    def getFreeRecorderList(self):
        """
        Returns a list of free recorders, or an empty list if none.
        """
        res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
        recorders = [int(d) for d in res]
        return recorders

    def getRecorderDetails(self, recorder_id):
        """
        Returns a Recorder object with details of the recorder.
        """
        c = self.db.cursor()
        c.execute("""SELECT cardid, cardtype, videodevice, hostname
                FROM capturecard WHERE cardid = %s""", recorder_id)
        row = c.fetchone()
        if row:
            recorder = Recorder(row)
            return recorder
        else:
            return None

    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
        """
        local = True
        cmd = 'LOCK_TUNER'
        if id is not None:
            cmd += ' %d' % id
            res = self.getRecorderDetails(id).hostname
            if res != socket.gethostname():
                local = False

        res = ''
        if local:
            res = self.backendCommand(cmd).split(BACKEND_SEP)
        else:
            myth = MythTV(res)
            res = myth.backendCommand(cmd).split(BACKEND_SEP)
            myth.close()
        res[0] = int(res[0])
        if res[0] > 0:
            self.locked_tuners.append(res[0])
        return tuple(res)
        

    def freeTuner(self,id=None):
        """
        Frees a requested tuner ID
        If no ID given, free all tuners listed as used by this class instance
        """
        def free(self,id):
            res = self.getRecorderDetails(id).hostname
            if res == socket.gethostname():
                self.backendCommand('FREE_TUNER %d' % id)
            else:
                myth = MythTV(res)
                myth.backendCommand('FREE_TUNER %d' % id)
                myth.close()

        if id is None:
            for i in range(len(self.locked_tuners)):
                free(self,self.locked_tuners.pop())
        else:
            try:
                self.locked_tuners.remove(id)
            except:
                pass
            free(self,id)   

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

    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

    def getRecording(self, chanid, starttime):
        """
        Returns a Program object matching the channel id and start time
        """
        res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' \
                        % (chanid, starttime)).split(BACKEND_SEP)
        if res[0] == 'ERROR':
            return None
        else:
            return Program(res[1:])

    def getRecordings(self):
        """
        Returns a list of all Program objects which have already recorded
        """
        programs = []
        res = self.backendCommand('QUERY_RECORDINGS Play').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        log.Msg(DEBUG, '%s total recordings', num_progs)
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    def getExpiring(self):
        """
        Returns a tuple of all Program objects nearing expiration
        """
        programs = []
        res = self.backendCommand('QUERY_GETEXPIRING').split(BACKEND_SEP)
        num_progs = int(res.pop(0))
        for i in range(num_progs):
            programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
                + PROGRAM_FIELDS]))
        return tuple(programs)

    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 deleteRecording(self,program,force=False):
        """
        Deletes recording, set 'force' true if file cannot be found
        Returns the number fewer recordings that exist afterwards
        """
        command = 'DELETE_RECORDING'
        if force:
            command = 'FORCE_DELETE_RECORDING'
        return self.backendCommand(BACKEND_SEP.join(\
                    [command,program.toString()]))

    def forgetRecording(self,program):
        """
        Forgets old recording and allows it to be re-recorded
        """
        self.backendCommand(BACKEND_SEP.join(['FORGET_RECORDING',
                    program.toString()]))

    def deleteFile(self,file,sgroup):
        """
        Deletes a file from specified storage group on the connected backend
        Takes a relative file path from the root of the storage group
        Returns 1 on success
        """
        return self.backendCommand(BACKEND_SEP.join(\
                    ['DELETE_FILE',file,sgroup]))

    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)
        dirs = []
        for i in range(0,len(res)/10):
            dirs.append(res[i*10:i*10+18])
        return tuple(dirs)

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

    def getLoad(self):
        """
        Returns a tuple of the 1, 5, and 15 minute load averages
        """
        res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP)
        return (float(res[0]),float(res[1]),float(res[2]))

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

    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:
                type,name,size = entry.split('::')
                if type == 'file':
                    files.append(name)
                    sizes.append(size)
                if type == 'dir':
                    dirs.append(name)
        if filenamesonly:
            return tuple(files)
        elif path == '':
            return tuple(dirs)
        else:
            return (tuple(dirs),tuple(files),tuple(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 getFrontends(self):
        """
        Returns a list of Frontend objects for accessible frontends
        """
        cursor = self.db.db.cursor()
        cursor.execute("""  SELECT DISTINCT hostname 
                            FROM settings
                            WHERE hostname IS NOT NULL
                            AND value='NetworkControlEnabled'
                            AND data=1""")
        frontends = []
        for fehost in cursor.fetchall():
            try:
                frontend = self.getFrontend(fehost[0])
                frontends.append(frontend)
            except:
                print "%s is not a valid frontend" % fehost[0]
        cursor.close()
        return frontends

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

    def getLastGuideData(self):
        """
        Returns the last dat for which guide data is available
        On error, 0000-00-00 00:00 is returned
        """
        return self.backendCommand('QUERY_GUIDEDATATHROUGH')

class FileTransfer(Backend):
    """
    A connection to mythbackend intended for file transfers
    """
    sockno = None
    tsize = 2**15
    open = False

    def __init__(self, file, type):
        r = re.compile(\
   'myth://((?P<group>.*)@)?(?P<host>[0-9\.]*)(:[0-9]*)?/(?P<file>.*)')
        if type not in ('r','w'):
            raise MythError("FileTransfer type must be read ('r') "+
                                "or write ('w')")
        self.db = MythDB(sys.argv[1:])
        if isinstance(file, Program):
            # process program object
            match = r.match(file.filename)
            self.host = match.group('host')
            self.filename = match.group('file')
            self.sgroup = file.storagegroup
        elif isinstance(file, tuple):
            if len(file) != 3:
                log.Msg(CRITICAL, 'Incorrect FileTransfer() input size')
                raise MythError('Incorrect FileTransfer input tuple size')
                sys.exit(1)
            else:
                # process ('host','filename','storagegroup')
                self.host = file[0]
                self.filename = file[1]
                self.sgroup = file[2]
        elif isinstance(file, str):
            # process URL (myth://<group>@<host>:<port>/<path/to/file>)
            match = r.match(file)
            if match is None:
                log.Msg(CRITICAL, 'Incorrect FileTransfer() input string: %s'\
                            % file)
                raise MythError('Incorrect FileTransfer input string')
            self.sgroup = match.group('group')
            self.host = match.group('host')
            self.filename = match.group('file')
            if self.sgroup is None:
                self.sgroup = 'Default'
        else:
            log.Msg(CRITICAL, 'Invalid file input to FileTransfer')
            raise MythError('Invalid file input to FileTransfer')

        # create control socket
        self.control = MythTV(self.host,'Playback')
        # continue normal Backend initialization
        Backend.__init__(self,self.host,type)
        self.open = True

    def announce(self, type):
        # replacement announce for Backend object
        if type == 'w':
            self.w = True
        elif type == 'r':
            self.w = False
        res = self.backendCommand('ANN FileTransfer %s %d %d %s' \
                    % (socket.gethostname(), self.w, False,
                        BACKEND_SEP.join(['-1',self.filename,self.sgroup])))
        if res.split(BACKEND_SEP)[0] != 'OK':
            log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
            raise MythError('Unexpected answer to ANN command: %s' % res)
        else:
            log.Msg(INFO, 'Successfully connected mythbackend at %s:%d',
                        self.host, self.port)
            sp = res.split(BACKEND_SEP)
            self.sockno = int(sp[1])
            self.pos = 0
            self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3])

    def __del__(self):
        if not self.open:
            return
        self.control.backendCommand('QUERY_FILETRANSFER '\
                    +BACKEND_SEP.join([str(self.sockno), 'JOIN']))
        self.socket.shutdown(1)
        self.socket.close()
        self.open = False

    def tell(self):
        """
        Return the current offset from the beginning of the file
        """
        return self.pos

    def close(self):
        """
        Close the data transfer socket
        """
        self.__del__()

    def rewind(self):
        """
        Seek back to the start of the file
        """
        self.seek(0)

    def read(self, size):
        """
        Read a block of data, requests over 64KB will be buffered internally
        """
        if self.w:
            print 'Error: attempting to read from a write-only socket'
            return
        if size == 0:
            return ''
        if size > self.size - self.pos:
            size = self.size - self.pos
        csize = size
        rsize = 0
        if csize > self.tsize:
            csize = self.tsize
            rsize = size - csize
            
        res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                    .join([str(self.sockno),'REQUEST_BLOCK',str(csize)]))
        self.pos += int(res)
#       if int(res) == csize:
#           if csize < size:
#               self.tsize += 8192
#       else:
#           self.tsize -= 8192
#           rsize = size - int(res)
#       print 'resizing buffer to %d' % self.tsize

        return self.socket.recv(int(res)) + self.read(rsize)

    def write(self, data):
        """
        Write a block of data, requests over 64KB will be buffered internally
        """
        if not self.w:
            print 'Error: attempting to write to a read-only socket'
            return
        size = len(data)
        buff = ''
        if size == 0:
            return
        if size > self.tsize:
            size = self.tsize
            buff = data[size:]
            data = data[:size]
        self.pos += int(self.socket.send(data))
        self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                    .join([str(self.sockno),'WRITE_BLOCK',str(size)]))
        self.write(buff)
        return
            

    def seek(self, offset, whence=0):
        """
        Seek 'offset' number of bytes
           whence==0 - from start of file
                   whence==1 - from current position
        """
        if whence == 0:
            if offset < 0:
                offset = 0
            if offset > self.size:
                offset = self.size
        elif whence == 1:
            if offset + self.pos < 0:
                offset = -self.pos
            if offset + self.pos > self.size:
                offset = self.size - self.pos
        elif whence == 2:
            if offset > 0:
                offset = 0
            if offset < -self.size:
                offset = -self.size
        else:
            log.Msg(CRITICAL, 'Whence can only be 0, 1, or 2')

        curhigh,curlow = self.splitInt(self.pos)
        offhigh,offlow = self.splitInt(offset)

        res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\
                        .join([str(self.sockno),'SEEK',str(offhigh),
                                str(offlow),str(whence),str(curhigh),
                                str(curlow)])\
                    ).split(BACKEND_SEP)
        self.pos = self.joinInt(res[0],res[1])

class Frontend(object):
    isConnected = False
    socket = None
    host = None
    port = None

    def __init__(self, host, port):
        self.host = host
        self.port = int(port)
        self.connect()
        self.disconnect()

    def __del__(self):
        if self.isConnected:
            self.disconnect()

    def __repr__(self):
        return "%s@%d" % (self.host, self.port)

    def __str__(self):
        return "%s@%d" % (self.host, self.port)

    def close(self): self.__del__()

    def connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(10)
        self.socket.connect((self.host, self.port))
        if re.search("MythFrontend Network Control.*",self.recv()) is None:
            self.socket.close()
            self.socket = None
            raise Exception('FrontendConnect',
                'Connected socket does not belong to a mythfrontend')
        self.isConnected = True

    def disconnect(self):
        self.send("exit")
        self.socket.close()
        self.socket = None
        self.isConnected = False

    def send(self,command):
        if not self.isConnected:
            self.connect()
        self.socket.send("%s\n" % command)

    def recv(self):
        curstr = ''
        prompt = re.compile('([\r\n.]*)\r\n# ')
        while not prompt.search(curstr):
            try:
                curstr += self.socket.recv(100)
            except socket.error:
                raise MythError('Frontend has closed connection')
            except KeyboardInterrupt:
                raise
        return prompt.split(curstr)[0]

    def sendJump(self,jumppoint):
        """
        Sends jumppoint to frontend
        """
        self.send("jump %s" % jumppoint)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getJump(self):
        """
        Returns a tuple containing available jumppoints
        """
        self.send("help jump")
        return tuple(re.findall('(\w+)[ ]+- ([\w /,]+)',self.recv()))

    def sendKey(self,key):
        """
        Sends keycode to connected frontend
        """
        self.send("key %s" % key)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getKey(self):
        """
        Returns a tuple containing available special keys
        """
        self.send("help key")
        return tuple(self.recv().split('\r\n')[4].split(', '))

    def sendQuery(self,query):
        """
        Returns query from connected frontend
        """
        self.send("query %s" % query)
        return self.recv()

    def getQuery(self):
        """
        Returns a tuple containing available queries
        """
        self.send("help query")
        return tuple(re.findall('query ([\w ]*\w+)[ \r\n]+- ([\w /,]+)',
                        self.recv()))

    def sendPlay(self,play):
        """
        Send playback command to connected frontend
        """
        self.send("play %s" % play)
        if self.recv() == 'OK':
            return 0
        else:
            return 1

    def getPlay(self):
        """
        Returns a tuple containing available playback commands
        """
        self.send("help play")
        return tuple(re.findall('play ([\w -:]*\w+)[ \r\n]+- ([\w /:,\(\)]+)',
                        self.recv()))
        

class Recorder(ListData):
    """
    Represents a MythTV capture card.
    """
    field_order = ['cardid','cardtype','videodevice','hostname']
    field_type = [2,2,2,2]
    def __str__(self):
        return "Recorder %s (%s)" % (self.cardid, self.cardtype)

    def __repr__(self):
        return "Recorder %s (%s)" % (self.cardid, self.cardtype)

class FreeSpace(ListData):
    """
    Represents a Storage Group entry
    """
    field_order = [ 'host',         'path',     'islocal',
                    'disknumber',   'sgroupid', 'blocksize',
                    'ts_high',      'ts_low',   'us_high',
                    'us_low']
    field_type = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0]
    def __str__(self):
        return "Storage Group %d (%s@%s)"\
                    % (self.sgroupid, self.path, self.host)

    def __repr__(self):
        return "Storage Group %d (%s@%s)"\
                    % (self.sgroupid, self.path, self.host)

    def __init__(self, data):
        ListData.__init__(self, data)
        self.totalspace = self.joinInt(self.ts_high, self.ts_low)
        self.usedspace = self.joinInt(self.us_high, self.us_low)
        self.freespace = self.totalspace - self.usedspace

class Program(ListData):
    """
    Represents a program with all the detail known.
    """

    # field names and type in order
    #   0: means integer
    #   1: means float
    #   2: means string
    #   3: means date
    field_order = [ 'title',        'subtitle',     'description',
                    'category',     'chanid',       'channum',
                    'callsign',     'channame',     'filename',
                    'fs_high',      'fs_low',       'starttime',
                    'endtime',      'duplicate',    'shareable',
                    'findid',       'hostname',     'sourceid',
                    'cardid',       'inputid',      'recpriority',
                    'recstatus',    'recordid',     'rectype',
                    'dupin',        'dupmethod',    'recstartts',
                    'recendts',     'repeat',       'programflags',
                    'recgroup',     'commfree',     'outputfilters',
                    'seriesid',     'programid',    'lastmodified',
                    'stars',        'airdate',      'hasairdate',
                    'playgroup',    'recpriority2', 'parentid',
                    'storagegroup', 'audio_props',  'video_props',
                    'subtitle_type','year']
    field_type = [  3,      3,      3,
                    3,      0,      3,
                    3,      3,      3,
                    0,      0,      4,
                    4,      0,      0,
                    0,      3,      0,
                    0,      0,      0,
                    0,      0,      3,
                    0,      0,      4,
                    4,      0,      3,
                    3,      0,      3,
                    3,      3,      3,
                    1,      3,      0,
                    3,      0,      3,
                    3,      0,      0,
                    0,      0]
    def __str__(self):
        return u"%s (%s)" % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __repr__(self):
        return u"%s (%s)".encode('utf-8') % (self.title,
                    self.starttime.strftime('%Y-%m-%d %H:%M:%S'))

    def __init__(self, data):
        ListData.__init__(self, data)
        self.filesize = self.joinInt(self.fs_high,self.fs_low)

    def toString(self):
        data = []
        for i in range(0,PROGRAM_FIELDS):
            if self.data[self.field_order[i]] == None:
                datum = ''
            elif self.field_type[i] in (0,1,2):
                datum = str(self.data[self.field_order[i]])
            elif self.field_type[i] == 3:
                datum = str(int(mktime(self.data[self.field_order[i]])))
            data.append(datum)
        return BACKEND_SEP.join(data)

    def setField(self,field,value):
        if field not in ['basename','hostname','storagegroup']:
            raise MythError('Invalid field name')
        db = MythDB()
        c = db.cursor()
        c.execute("""UPDATE recorded SET %s = %%s
                WHERE chanid=%%s and starttime=%%s""" % field,
                (value,self.chanid,self.starttime))
        c.close()
        db.close()

    def setFilename(self,name):
        """
        Change the file basename pointed to by the recording
        """
        self.setField('basename',name)
        self.filename = name

    def setHostname(self,name):
        """
        Change the hostname of the machine which holds the recording
        """
        self.setField('hostname',name)
        self.hostname = name

    def setSG(self,name):
        """
        Change the storagegroup which holds the recording
        """
        self.setField('storagegroup',name)
        self.storagegroup = storagegroup


if __name__ == '__main__':
    banner = '\'m\' is a MythTV instance.'
    try:
        import readline, rlcompleter
    except:
        pass
    else:
        readline.parse_and_bind("tab: complete")
        banner = banner + " TAB completion is available."
    m = MythTV()
    namespace = globals().copy()
    namespace.update(locals())
    code.InteractiveConsole(namespace).interact(banner)

# vim: set ts=4 sw=4 expandtab:
