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

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

from MythStatic import *

import os, re, socket, MySQLdb, sys
import xml.dom.minidom as minidom
from datetime import datetime
from urllib import urlopen
from subprocess import Popen
from sys import version_info


class DictData( object ):
    """
    Basic class for providing management of data resembling dictionaries.
    Must be subclassed for use.

    Subclasses must provide:
        field_order
            string list of field names
        field_type
            integer list of field types by:
                0: integer  - int(value)
                1: float    - float(value)
                2: bool     - bool(value)
                3: string   - value
                4: date     - datetime.fromtimestamp(int(value))
            can be the single string 'Pass', to pass all values untouched

    Data can be accessed as:
        obj.field_name
            -- or --
        obj['field_name']
    """
    logmodule = 'Python DictData'

    ### class emulation functions
    def __getattr__(self,name):
        # function for class attribute access of data fields
        # accesses real attributes, falls back to data fields, and errors
        if name in self.__dict__:
            return self.__dict__[name]
        elif name in self.field_order:
            return self.data[name]
        else:
            raise AttributeError("'DictData' object has no attribute '%s'"%name)

    def __setattr__(self,name,value):
        # function for class attribute access of data fields
        # sets value in data fields, falling back to real attributes
        if name in self.field_order:
            self.data[name] = value
        else:
            self.__dict__[name] = value

    ### container emulation functions
    def __getitem__(self,key):
        # function for and dist-like access to content
        # as a side effect of dual operation, dict data cannot be indexed
        #       keyed off integer values
        if key in self.data:
            return self.data[key]
        else:
            raise KeyError("'DictData' object has no key '%s'" %key)

    def __setitem__(self,key,value):
        # function for dist-like access to content
        # does not allow setting of data not already in field_order
        if key in self.field_order:
            self.data[key] = value
        else:
            raise KeyError("'DictData' does not allow new data")

    def __contains__(self,item):
        return bool(item in self.data.keys())

    def __iter__(self):
        self.field = 0
        self.itermode = 0
        return self
        
    def iterkeys(self):
        self.field = 0
        self.itermode = 1
        return self
        
    def itervalues(self):
        self.field = 0
        self.itermode = 0
        return self
    
    def iteriterms(self):
        self.field = 0
        self.itermode = 2
        return self

    def next(self):
        if self.field == len(self.field_order):
            del self.field
            del self.itermode
            raise StopIteration
        self.field += 1
        if self.itermode == 0:
            return self.data[self.field_order[self.field-1]]
        elif self.itermode == 1:
            return self.field_order[self.field-1]
        elif self.itermode == 2:
            return (self.field_order[self.field-1],
                    self.data[self.field_order[self.field-1]])
        
    def keys(self):
        return self.field_order
        
    def has_key(self,item):
        return self.__contains__(item)
        
    def values(self):
        return list(self)
        
    def get(self,key):
        return self.data[key]
        
    def items(self):
        pairs = []
        for field in self.field_order:
            pairs.append((field, self.data[field]))
        return tuple(pairs)
        
    def pop(self,key):
        raise NotImplementedError
    
    def popitem(self):
        raise NotImplementedError
        
    def update(self, *args, **keywords):
        self.data.update(*args, **keywords)

    ### additional management functions
    def _setDefs(self):
        self.data = {}
        self.field = 0
        self.log = MythLog(self.logmodule)
        
    def __init__(self, raw):
        self._setDefs()
        self.data.update(self._process(raw))

    def _process(self, data):
        data = list(data)
        for i in range(0, len(data)):
            if data[i] == '':
                data[i] = None
            elif self.field_type == 'Pass':
                data[i] = data[i]
            elif self.field_type[i] == 0:
                data[i] = int(data[i])
            elif self.field_type[i] == 1:
                data[i] = float(data[i])
            elif self.field_type[i] == 2:
                data[i] = bool(data[i])
            elif self.field_type[i] == 3:
                data[i] = data[i]
            elif self.field_type[i] == 4:
                data[i] = datetime.fromtimestamp(int(data[i]))
        return dict(zip(self.field_order,data))

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

    def listFields(self):
        """
        Returns a tuple of all available object fields.
        """
        return tuple(self.data.keys())

class DBData( DictData ):
    """
    Altered DictData adding several functions for dealing with individual rows.
    Must be subclassed for use.

    Subclasses must provide:
        table
            Name of database table to be accessed
        where
            String defining WHERE clause for database lookup
        setwheredat
            String of comma separated variables to be evaluated to set
            'wheredat'. 'eval(setwheredat)' must return a tuple, so string must 
            contain at least one comma.

    Subclasses may provide:
        allow_empty
            Controls whether DBData() will allow an empty instance.
    """
    field_type = 'Pass'
    allow_empty = False
    logmodule = 'Python DBData'

    def _setDefs(self):
        self.__dict__['field_order'] = self.db.tablefields[self.table]
        DictData._setDefs(self)
        self._fillNone()
        self.wheredat = None
        self.log = MythLog(self.logmodule, db=self.db)

    def __init__(self, data=None, db=None, raw=None):
        self.__dict__['db'] = MythDBConn(db)
        self._setDefs()

        if raw is not None:
            if len(raw) == len(self.field_order):
                self.data.update(self._process(raw))
                self.wheredat = eval(self.setwheredat)
            else:
                raise MythError('Incorrect raw input length to DBData()')
        elif data is not None:
            self.wheredat = tuple(data)
            self._pull()
        else:
            if self.allow_empty:
                self._fillNone()
            else:
                raise MythError('DBData() not given sufficient information')

    def _pull(self):
        """Updates table with data pulled from database."""
        c = self.db.cursor()
        query = """SELECT * FROM %s WHERE %s""" % (self.table, self.where)
        self.log.log(self.log.DATABASE, query, str(self.wheredat))
        c.execute(query, self.wheredat)
        data = c.fetchone()
        c.close()
        if data is None:
            return
        self.data.update(self._process(data))

    def _fillNone(self):
        """Fills out dictionary fields with empty data"""
        self.field_order = self.db.tablefields[self.table]
        for field in self.field_order:
            self.data[field] = None

class DBDataWrite( DBData ):
    """
    Altered DBData, with support for writing back to the database.
    Must be subclassed for use.

    Subclasses must provide:
        table
            Name of database table to be accessed
        where
            String defining WHERE clause for database lookup
        setwheredat
            String of comma separated variables to be evaluated to set
            'wheredat'. 'eval(setwheredat)' must return a tuple, so string must 
            contain at least one comma.

    Subclasses may provide:
        defaults
            Dictionary of default values to be used when creating new 
            database entries. Additionally, values of 'None' will be stripped
            and not used to alter the database.
    """
    defaults = None
    allow_empty = True
    logmodule = 'Python DBDataWrite'

    def _sanitize(self,data):
        """Remove fields from dictionary that are not in database table."""
        for key in data.keys():
            if key not in self.field_order:
                del data[key]
        if self.defaults is not None:
            for key in self.defaults:
                if key in data:
                    if self.defaults[key] is None:
                        del data[key]
                    elif data[key] is None:
                        data[key] = self.defaults[key]
        return data

    def _setDefs(self):
        DBData._setDefs(self)
        self._fillDefs()

    def _fillDefs(self):
        self._fillNone()
        self.data.update(self.defaults)

    def create(self,data=None):
        """
        Creates a new database entry using given dictionary.
        Only works on an uninitialized object.
        """
        if data is None:
            data = self.data
        elif self.wheredat is not None:
            raise MythError('DBDataWrite object already bound to '+\
                                                    'existing instance')

        data = self._sanitize(data)
        self.data = {}
        self._fillNone()
        self.data.update(data)
        data = self._sanitize(self.data)
        c = self.db.cursor()
        fields = ', '.join(data.keys())
        format_string = ', '.join(['%s' for d in data.values()])
        query = """INSERT INTO %s (%s) VALUES(%s)""" \
                        % (self.table, fields, format_string)
        self.log.log(self.log.DATABASE, query, str(data.values()))
        c.execute(query, data.values())
        intid = c.lastrowid
        c.close()
        return intid

    def _push(self):
        """
        Updates database with data from local table.
        """
        if (self.where is None) or (self.wheredat is None):
            return
        c = self.db.cursor()
        data = self._sanitize(self.data)
        format_string = ', '.join(['%s = %%s' % d for d in data])
        sql_values = data.values()
        sql_values.extend(self.wheredat)

        query = """UPDATE %s SET %s WHERE %s"""\
                    % (self.table, format_string, self.where)
        self.log.log(self.log.DATABASE, query, str(sql_values))
        c.execute(query, sql_values)
        c.close()
        self._pull()
        
    def update(self, *args, **keywords):
        data = {}
        data.update(*args, **keywords)
        self.data.update(self._sanitize(data))
        self._push()

    def delete(self):
        """
        Delete video entry from database.
        """
        if (self.where is None) or \
                (self.wheredat is None) or \
                (self.data is None):
            return
        c = self.db.cursor()
        query = """DELETE FROM %s WHERE %s""" % (self.table, self.where)
        self.log.log(self.log.DATABASE, query, str(self.wheredat))
        c.execute(query, self.wheredat)
        c.close()

class DBDataCRef( object ):
    """
    Class for managing lists of crossreferenced data.
    Requires the additional fields:
        table   - primary data table
        rtable  - cross reference table
        t_ref   - primary table field to align with cross reference table
        r_ref   - cross reference table field to align with primary table
        t_add   - list of fields for data stored in primary table
        r_add   - list of fields for data stored in cross reference table
        w_field - list of fields for WHERE argument in lookup
    """
    logmodule = 'Python DBDataCRef'

    class SubData( DictData ):
        def _setDefs(self):
            self.__dict__['field_order'] = []
            DictData._setDefs(self)
        def __repr__(self):
            return str(tuple(self.data.values()))
        def __init__(self,data,fields):
            self._setDefs()
            self.field_order = ['cross']+fields
            self.field_type = 'Pass'
            self.data.update(self._process(data))
        def __hash__(self):
            dat = self.data.copy()
            del dat['cross']
            keys = dat.keys()
            keys.sort()
            return hash(str([hash(dat[k]) for k in keys]))
        def __cmp__(self, x):
            return cmp(hash(self),hash(x))

    def _setDefs(self):
        self.data = None
        self.hash = None
        self.log = MythLog(self.logmodule, db=self.db)

    def __repr__(self):
        if self.data is None:
            self.fill()
        return "<DBDataCRef '%s' at %s>" % \
                        (str(tuple(self.data)), hex(id(self)))

    def __getitem__(self,index):
        if self.data is None:
            self._fill()
        if index in range(-len(self.data),0)+range(len(self.data)):
            return self.data[index]
        else:
            raise IndexError

    def __iter__(self):
        self.field = 0
        if self.data is None:
            self._fill()
        return self

    def next(self):
        if self.field == len(self.data):
            del self.field
            raise StopIteration
        self.field += 1
        return self[self.field-1]

    def __init__(self,where,db=None):
        self.db = MythDBConn(db)
        self._setDefs()
        self.w_dat = tuple(where)

    def _fill(self):
        self.data = []
        self.hash = []
        fields = ','.join([self.table+'.'+self.t_ref]+self.t_add+self.r_add)
        where = ' AND '.join('%s=%%s' % d for d in self.w_field)
        join = '%s.%s=%s.%s' % (self.table,self.t_ref,self.rtable,self.r_ref)
        c = self.db.cursor()
        query = """SELECT %s FROM %s JOIN %s ON %s WHERE %s""" \
                    % (fields, self.table, self.rtable, join, where)
        self.log.log(self.log.DATABASE, query, str(self.w_dat))
        c.execute(query, self.w_dat)
        for entry in c.fetchall():
            self.data.append(self.SubData(entry, self.t_add+self.r_add))
            self.hash.append(hash(self.data[-1]))

    def _tdata(self, sub):
        return tuple([sub[key] for key in self.t_add])

    def _rdata(self, sub):
        return tuple([sub[key] for key in self.r_add+['cross']]+list(self.w_dat))

    def _search(self, sub):
        for i in range(len(self.data)):
            if self.data[i] == sub:
                return i

    def add(self,data):
        if self.data is None:
            self._fill()
        if len(data) != len(self.t_add+self.r_add):
            return

        # check for existing
        dat = self.SubData([0]+list(data),self.t_add+self.r_add)
        if dat in self.data:
            return

        tdata = self._tdata(dat)

        c = self.db.cursor()
        # search for existing table data, add if needed
        where = ' AND '.join('%s=%%s' % d for d in self.t_add)
        query = """SELECT %s FROM %s WHERE %s""" \
                            % (self.t_ref, self.table, where)
        self.log.log(self.log.DATABASE, query, str(self._tdata(dat)))
        if c.execute(query, self._tdata(dat)) > 0:
            id = c.fetchone()[0]
        else:
            fields = ', '.join(self.t_add)
            format = ', '.join(['%s' for d in self.t_add])
            query = """INSERT INTO %s (%s) VALUES(%s)""" \
                            % (self.table, fields, format)
            self.log(self.log.DATABASE, query, str(self._tdata(dat)))
            c.execute(query, self._tdata(dat))
            id = c.lastrowid

        dat = self.SubData([id]+list(data),self.t_add+self.r_add)
            
        # add cross reference
        fields = ', '.join(self.r_add+[self.r_ref]+self.w_field)
        format = ', '.join(['%s' for d in fields.split(', ')])
        query = """INSERT INTO %s (%s) VALUES(%s)""" \
                            % (self.rtable, fields, format)
        self.log.log(self.log.DATABASE, query, str(self._rdata(dat)))
        c.execute(query, self._rdata(dat))
        c.close()

        # add local entry
        self.data.append(dat)

    def delete(self,data):
        if self.data is None:
            self._fill()
        # check for local entry
        dat = self.SubData([0]+list(data),self.t_add+self.r_add)
        if dat not in self.data:
            return
        # remove local entry
        index = self._search(dat)
        dat = self.data.pop(index)
        
        # remove database cross reference
        fields = self.r_add+[self.r_ref]+self.w_field
        where = ' AND '.join('%s=%%s' % d for d in fields)
        c = self.db.cursor()
        query = """DELETE FROM %s WHERE %s""" % (self.rtable, where)
        self.log(self.log.DATABASE, query, str(self._rdata(dat)))
        c.execute(query, self._rdata(dat))

        # remove primary entry if unused
        query = """SELECT %s FROM %s WHERE %s=%%s""" \
                            % (self.r_ref, self.rtable, self.r_ref)
        self.log(self.log.DATABASE, query, str(dat.cross))
        if c.execute(query, dat.cross) == 0:
            query = """DELETE FROM %s WHERE %s=%%s""" % (self.table, self.t_ref)
            self.log(self.log.DATABASE, query, str(dat.cross))
            c.execute(query, dat.cross)
        c.close()

class MythDBConn( object ):
    """
    Basic connection to the mythtv database
    Offers connection caching to prevent multiple connections
    """
    db = None
    logmodule = 'Python Database Connection'

    def __repr__(self):
        return "<%s '%s' at %s>" % \
                (str(self.__class__).split("'")[1].split(".")[-1], 
                 self.ident, hex(id(self)))

    class _TableFields(object):
        """Provides a dictionary-like list of table fieldnames"""
        def __init__(self, ident):
            self.log = MythLog('Python Database Connection')
            self._ident = ident
        def __getattr__(self,key):
            if key in self.__dict__:
                return self.__dict__[key]
            elif key in MYTHDBCONN[self._ident][2]:
                return MYTHDBCONN[self._ident][2][key]
            else:
                return self.__getitem__(key)
        def __getitem__(self,key):
            if key in MYTHDBCONN[self._ident][2]:
                return MYTHDBCONN[self._ident][2][key]
            table_names = []
            c = MYTHDBCONN[self._ident][0].cursor()
            try:
                self.log.log(self.log.DATABASE, 'DESC %s' % key)
                c.execute("DESC %s" % (key,))
            except:
                return None
            
            for name in c.fetchall():
                table_names.append(name[0])
            c.close()
            MYTHDBCONN[self._ident][2][key] = tuple(table_names)
            return table_names

    class _Settings(object):
        """Provides dictionary-like list of hosts"""
        class __Settings(object):
            """Provides dictionary-like list of settings"""
            def __repr__(self):
                return str(self.data.keys())
            def __init__(self,ident,host):
                self.__dict__['data'] = {}
                self._ident = ident
                self._host = host
                self.log = MythLog('Python Database Connection')
            def __getattr__(self,name):
                if name in self.__dict__:
                    return self.__dict__[name]
                else:
                    return self.__getitem__(name)
            def __setattr__(self,name,value):
                if name in ('data','_ident','_host', 'log'):
                    self.__dict__[name] = value
                else:
                    self.__setitem__(name,value)
            def __getitem__(self,key):
                if key in self.data:
                    return self.data[key]
                if self._host == 'NULL':
                    where = 'IS NULL'
                    wheredat = (key,)
                else:
                    where = 'LIKE(%s)'
                    wheredat = (key, self._host)
                c = MYTHDBCONN[self._ident][0].cursor()
                query = """SELECT data FROM settings WHERE value=%%s
                            AND hostname %s LIMIT 1""" % where
                self.log.log(self.log.DATABASE, query, str(wheredat))
                c.execute(query, wheredat)
                row = c.fetchone()
                c.close()
                if row:
                    self.data[key] = row[0]
                    return row[0]
                else:
                    return None
            def __setitem__(self, key, value):
                if key not in self.data:
                    self.__getitem__(key)
                c = MYTHDBCONN[self._ident][0].cursor()
                if key not in self.data:
                    query = """INSERT INTO settings (value,data,hostname)
                                        (%s,%s,%s)"""
                    self.log(self.log.DATABASE, query, 
                                        str((key, value, self._host)))
                    c.execute(query, (key, value, self._host))
                else:
                    query = """UPDATE settings SET data=%s
                                        WHERE value=%s AND hostname=%s"""
                    self.log(self.log.DATABASE, query,
                                        str((value, key, self._host)))
                    c.execute(query, (value, key, self._host))
                self.data[key] = value
        def __repr__(self):
            return str(MYTHDBCONN[self._ident][3].keys())
        def __init__(self, ident):
            self._ident = ident
        def __getattr__(self,key):
            if key in self.__dict__:
                return self.__dict__[key]
            else:
                return self.__getitem__(key)
        def __getitem__(self,key):
            if key not in MYTHDBCONN[self._ident][3]:
                MYTHDBCONN[self._ident][3][key] =\
                             self.__Settings(self._ident,key)
            return MYTHDBCONN[self._ident][3][key]

    def __init__(self, db=None, args=None, **dbconn):
        self.log = MythLog(self.logmodule)
        if db is not None:
            # load existing database connection
            self.log.log(MythLog.DATABASE, "Loading existing connection",
                                    str(db.dbconn))
            dbconn.update(db.dbconn)
        if args is not None:
            # load user defined arguments (takes dict, or key/value tuple)
            self.log.log(MythLog.DATABASE, "Loading user settings", str(args))
            dbconn.update(args)
        if not self._check_dbconn(dbconn):
            # insufficient information for connection given
            # try to read from config.xml
            config_files = [ os.path.expanduser('~/.mythtv/config.xml') ]
            if 'MYTHCONFDIR' in os.environ:
                config_files.append('%s/config.xml' %os.environ['MYTHCONFDIR'])

            dbconn = {'DBHostName':0,   'DBName':0, 'DBUserName':0,
                      'DBPassword':0,   'USN':0,    'SecurityPin':0}

            for config_file in config_files:
                try:
                    config = minidom.parse(config_file)
                except:
                    continue

                for key in dbconn:
                    dbconn[key] = None

                for token in config.getElementsByTagName('Configuration')[0].\
                                getElementsByTagName('UPnP')[0].\
                                getElementsByTagName('MythFrontend')[0].\
                                getElementsByTagName('DefaultBackend')[0].\
                                childNodes:
                    if token.nodeType == token.TEXT_NODE:
                        continue
                    try:
                        if token.tagName in dbconn:
                            dbconn[token.tagName] = token.childNodes[0].data
                    except:
                        pass

                if self._check_dbconn(dbconn):
                    self.log.log(MythLog.IMPORTANT|MythLog.DATABASE,
                           "Using connection settings from %s" % config_file)
                    break
            else:
                raise MythError("Could not find database login settings")

        self.dbconn = dbconn
        self.ident = "sql://%s@%s/" % \
                (dbconn['DBName'],dbconn['DBHostName'])
        if self.ident in MYTHDBCONN:
            # reuse existing database connection and update count
            self.db = MYTHDBCONN[self.ident][0]
            MYTHDBCONN[self.ident][1] += 1
        else:
            # attempt new connection
            self.connect()

            # check schema version
            sver = self._check_schema('DBSchemaVer',SCHEMA_VERSION)
            if sver:
                self.log.log(MythLog.DATABASE|MythLog.IMPORTANT, 
                            "DB speaks schema %d, but we speak %d" %\
                            (sver, SCHEMA_VERSION))
                self.db.close()
                self.db = None
                raise MythError('Mismatched schema version')

            # add connection to cache
            MYTHDBCONN[self.ident] = [self.db, 1, {}, {}]

        # connect to table name cache
        self.tablefields = self._TableFields(self.ident)
        self.settings = self._Settings(self.ident)

    def connect(self):
        try:
            self.log.log(MythLog.DATABASE, "Attempting connection", 
                                                str(self.dbconn))
            self.db = MySQLdb.connect(  user=   self.dbconn['DBUserName'],
                                        host=   self.dbconn['DBHostName'],
                                        passwd= self.dbconn['DBPassword'],
                                        db=     self.dbconn['DBName'],
                                        use_unicode=True,
                                        charset='utf8')
        except:
            raise MythError("Connection failed for '%s'@'%s' to database "
                     "%s using password %s" % ( self.dbconn['DBUserName'],
                                                self.dbconn['DBHostName'],
                                                self.dbconn['DBName'],
                                                self.dbconn['DBPassword']))

    def reconnect(self):
        self.connect()
        MYTHDBCONN[self.ident][0].close()
        MYTHDBCONN[self.ident][0] = self.db

    def _check_dbconn(self,dbconn):
        reqs = ['DBHostName','DBName','DBUserName','DBPassword']
        for req in reqs:
            if req not in dbconn:
                return False
            if dbconn[req] is None:
                return False
        return True

    def _check_schema(self,value,local):
        c = self.db.cursor()
        query = """SELECT data FROM settings WHERE value LIKE(%s)"""
        self.log.log(MythLog.DATABASE, query, str(value))
        c.execute(query,(value,))
        sver = int(c.fetchone()[0])
        c.close()

        if local == sver:
            return 0
        else:
            return sver

    def getStorageGroup(self, groupname=None, hostname=None):
        """
        Returns tuple of dictionaries containing storage group directories
            with the fields 'id', 'groupname', 'hostname', and 'dirname'
        Takes an optional groupname and hostname for filtering
        """
        c = self.db.cursor()
        where = []
        wheredat = []
        if groupname:
            where.append("groupname=%s")
            wheredat.append(groupname)
        if hostname:
            where.append("hostname=%s")
            wheredat.append(hostname)
        if len(where):
            where = 'WHERE '+' AND '.join(where)
            query = """SELECT * FROM storagegroup %s ORDER BY id""" % where
            self.log.log(MythLog.DATABASE, query, str(wheredat))
            c.execute(query, wheredat)
        else:
            query = """SELECT * FROM storagegroup ORDER BY id"""
            self.log.log(MythLog.DATABASE, query)
            c.execute(query)

        ret = []
        for row in c.fetchall():
            ret.append(StorageGroup(raw=row,db=self))
        return tuple(ret)

    def cursor(self):
        self.db.ping(True)
        return self.db.cursor()

    def close(self): self.__del__()

    def __del__(self):
        if self.db is not None:
            # decrement use count
            MYTHDBCONN[self.ident][1] -= 1
            self.db = None

            if not MYTHDBCONN[self.ident][1]:
                # database connection no longer in use, close and delete it
                MYTHDBCONN[self.ident][0].close()
                del MYTHDBCONN[self.ident]

class MythBEConn( object ):
    """
    Basic class for mythbackend socket connections
    """
    logmodule = 'Python Backend Connection'

    def __repr__(self):
        return "<%s 'myth://%s:%d/' at %s>" % \
                (str(self.__class__).split("'")[1].split(".")[-1], 
                 self.host, self.port, hex(id(self)))

    def __init__(self, backend=None, type='Monitor', db=None):
        self.connected = False
        self.db = MythDBConn(db)
        self.log = MythLog(self.logmodule, db=self.db)
        if backend is None:
            # use master backend, no sanity checks, these should always be set
            self.host = self.db.settings.NULL.MasterServerIP
            self.port = self.db.settings.NULL.MasterServerPort
        else:
            if re.match('(?:\d{1,3}\.){3}\d{1,3}',backend):
                # process ip address
                c = self.db.cursor()
                query = """SELECT hostname FROM settings WHERE
                            value='BackendServerIP' AND data=%s"""
                self.log.log(MythLog.DATABASE, query, str(backend))
                if c.execute(query, backend) == 0:
                    c.close()
                    raise MythError('Backend not found in database: '+backend)
                backend = c.fetchone()[0]
                c.close()
            self.host = self.db.settings[backend].BackendServerIP
            if not self.host:
                    raise MythError('Backend not found in database: '+backend)
            self.port = self.db.settings[backend].BackendServerPort
            if not self.port:
                raise MythError('Backend port not found in database: '+backend)
        self.port = int(self.port)

        try:
            self.log.log(MythLog.SOCKET|MythLog.NETWORK,
                    "Connecting to backend %s:%d" % (self.host, self.port))
            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)
            self.connected = True
        except socket.error, e:
            self.log.log(MythLog.IMPORTANT|MythLog.SOCKET,
                    "Couldn't connect to backend %s:%d" % (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):
        if self.connected:
            self.log.log(MythLog.SOCKET|MythLog.NETWORK,
                    "Terminating connection to %s:%d" % (self.host, self.port))
            self.backendCommand('DONE')
            self.socket.shutdown(1)
            self.socket.close()
            self.connected = False

    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':
            self.log.log(MythLog.IMPORTANT|MythLog.NETWORK, 
                            "Backend has version %s, and we speak %d" %\
                            (res[1], PROTO_VERSION))
            raise MythError('Backend has version %s '+
                    'and we speak version %d' % (res[1], PROTO_VERSION))

    def announce(self,type):
        res = self.backendCommand('ANN %s %s 0' % (type, socket.gethostname()))
        if res != 'OK':
            self.log.log(MythLog.IMPORTANT|MythLog.NETWORK,
                            "Unexpected answer to ANN", res)
            raise MythError('Unexpected answer to ANN command: %s' % res)
        else:
            self.log.log(MythLog.SOCKET,"Successfully connected to backend",
                                        "%s:%d" % (self.host, self.port))
        self.hostname = self.backendCommand('QUERY_HOSTNAME')

    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)
        self.log.log(MythLog.NETWORK, "Sending command", 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 MythXMLConn( object ):
    def __repr__(self):
        return "<%s 'http://%s:%d/' at %s>" % \
                (str(self.__class__).split("'")[1].split(".")[-1], 
                 self.host, self.port, hex(id(self)))

    def __init__(self, backend=None, db=None):
        db = MythDBConn(db)
        self.log = MythLog('Python XML Connection', db=db)
        if backend is None:
            # use master backend
            self.host = db.settings.NULL.MasterServerIP
            self.port = 6544 #self.db.settings.NULL.MasterServerPort
        elif re.match('(?:\d{1,3}\.){3}\d{1,3}',backend):
            # process ip address
            self.host = backend
            c = db.cursor()
            query = """SELECT hostname FROM settings WHERE
                        value='BackendServerIP' AND data=%s"""
            self.log.log(MythLog.DATABASE, query, str(self.host))
            if c.execute(query, self.host) == 0:
                backend = None
            else:
                backend = c.fetchone()[0]
            self.port = 6544 #self.db.settings[backend].BackendServerPort
            c.close()
        else:
            # assume given a hostname
            self.host = db.settings[backend].BackendServerIP
            if not self.host:
                # try a truncated hostname
                backend = backend.split('.')[0]
                self.host = db.setting[backend].BackendServerIP
                if not self.host:
                    raise MythError('Unable to find requested backend.')
            self.port = 6544 #self.db.setting[backend].BackendServerPort
        db.close()

    def _query(self, path=None, **keyvars):
        url = 'http://%s:%d/' % (self.host, self.port)
        if path is not None:
            url += 'Myth/%s/' % path
        if len(keyvars) > 0:
            fields = []
            for key in keyvars:
                fields.append('%s=%s' % (key, keyvars[key]))
            url += '?'+'&'.join(fields)
        print 'opening %s' % url
        ufd = urlopen(url)
        res = minidom.parseString(ufd.read())
        ufd.close()
        return res
    

class MythLog( object ):
    """
    A simple logging class
    """

    ALL         = 0b1111111111111111111111111111
    MOST        = 0b0011111111101111111111111111
    NONE        = 0b0000000000000000000000000000

    IMPORTANT   = 0b0000000000000000000000000001
    GENERAL     = 0b0000000000000000000000000010
    RECORD      = 0b0000000000000000000000000100
    PLAYBACK    = 0b0000000000000000000000001000
    CHANNEL     = 0b0000000000000000000000010000
    OSD         = 0b0000000000000000000000100000
    FILE        = 0b0000000000000000000001000000
    SCHEDULE    = 0b0000000000000000000010000000
    NETWORK     = 0b0000000000000000000100000000
    COMMFLAG    = 0b0000000000000000001000000000
    AUDIO       = 0b0000000000000000010000000000
    LIBAV       = 0b0000000000000000100000000000
    JOBQUEUE    = 0b0000000000000001000000000000
    SIPARSER    = 0b0000000000000010000000000000
    EIT         = 0b0000000000000100000000000000
    VBI         = 0b0000000000001000000000000000
    DATABASE    = 0b0000000000010000000000000000
    DSMCC       = 0b0000000000100000000000000000
    MHEG        = 0b0000000001000000000000000000
    UPNP        = 0b0000000010000000000000000000
    SOCKET      = 0b0000000100000000000000000000
    XMLTV       = 0b0000001000000000000000000000
    DVBCAM      = 0b0000010000000000000000000000
    MEDIA       = 0b0000100000000000000000000000
    IDLE        = 0b0001000000000000000000000000
    CHANNELSCAN = 0b0010000000000000000000000000
    EXTRA       = 0b0100000000000000000000000000
    TIMESTAMP   = 0b1000000000000000000000000000

    DBALL       = 8
    DBDEBUG     = 7
    DBINFO      = 6
    DBNOTICE    = 5
    DBWARNING   = 4
    DBERROR     = 3
    DBCRITICAL  = 2
    DBALERT     = 1
    DBEMERGENCY = 0

    LOGLEVEL = IMPORTANT|GENERAL

    def __repr__(self):
        return "<%s '%s','%s' at %s>" % \
                (str(self.__class__).split("'")[1].split(".")[-1], 
                 self.module, bin(self.LOGLEVEL), hex(id(self)))

    def __init__(self, module='pythonbindings', lstr=None, lbit=None, db=None):
        self._setlevel(lstr, lbit)
        self.module = module
#        self.db = db
#        if self.db:
#            self.db = MythDBConn(self.db)

    def _setlevel(lstr=None, lbit=None):
        if lstr:
            MythLog.LOGLEVEL = MythLog._parselevel(lstr)
        elif lbit:
            MythLog.LOGLEVEL = lbit
    _setlevel = staticmethod(_setlevel)

    def _parselevel(lstr):
        level = MythLog.NONE
        bwlist = (  'important','general','record','playback','channel','osd',
                    'file','schedule','network','commflag','audio','libav',
                    'jobqueue','siparser','eit','vbi','database','dsmcc',
                    'mheg','upnp','socket','xmltv','dvbcam','media','idle',
                    'channelscan','extra','timestamp')
        for l in lstr.split(','):
            if l in ('all','most','none'):
                # set initial bitfield
                level = eval('MythLog.'+l.upper())
            elif l in bwlist:
                # update bitfield OR
                level |= eval('MythLog.'+l.upper())
            elif len(l) > 2:
                if l[0:2] == 'no':
                    if l[2:] in bwlist:
                        # update bitfield NOT
                        level &= level^eval('MythLog.'+l[2:].upper())
        return level
    _parselevel = staticmethod(_parselevel)

    def log(self, level, message, detail=None, dblevel=None):
        if level&self.LOGLEVEL:
            if (version_info[0]>2) | (version_info[1]>5): # 2.6 or newer
                nowstr = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
            else:
                nowstr = datetime.now().strftime('%Y-%m-%d %H:%M:%S.000')
            if detail:
                print "%s %s: %s -- %s" %(nowstr, self.module, message, detail)
            else:
                print "%s %s: %s" % (nowstr, self.module, message)
        return

        if dblevel is not None:
            if self.db is None:
                self.db = MythDBConn()
            c = self.db.cursor()
            c.execute("""INSERT INTO mythlog (module,   priority,   logdate,
                                              host,     message,    details)
                                    VALUES   (%s, %s, %s, %s, %s, %s)""",
                                    (self.module,   dblevel,    now(),
                                     self.host,     message,    detail))
            c.close()

class MythError( Exception ):
    """
    A simple exception class
    """
    def __init__(self, message):
        self.message = message

    def __repr__(self):
        return repr(self.message)

class StorageGroup( DBData ):
    """
    Represents a single storage group entry
    """
    table = 'storagegroup'
    where = 'id=%s'
    setwheredat = 'self.id,'
    logmodule = 'Python StorageGroup'
    
    def __str__(self):
        return u"<StorageGroup 'myth://%s@%s%s' at %s" % \
                    (self.groupname, self.hostname,
                        self.dirname, hex(id(self)))

    def __repr__(self): return str(self).encode('utf-8')

    def __init__(self, id=None, db=None, raw=None):
        DBData.__init__(self, (id,), db, raw)
        if (self.hostname == socket.gethostname()) or \
              os.access(self.dirname, os.F_OK):
            self.local = True
        else:
            self.local = False

class Grabber( MythDBConn ):
    """
    A simple class for running metadata grabbers from the command line
    """
    logmodule = 'Python Metadata Grabber'

    def __init__(self, path=None, setting=None, db=None):
        """Sets up grabber using either a database setting or a direct path."""
        MythDBConn.__init__(self, db=db)
        self.log = MythLog(self.logmodule, db=self)
        self.path = ''
        if path:
            self.path = path
        elif setting:
            self.path = self.settings[socket.gethostname()][setting]
        self.returncode = 0
        self.stderr = ''

    def __str__(self):
        return "<%s '%s' at %s>" % \
                (str(self.__class__).split("'")[1].split(".")[-1], 
                    self.path, hex(id(self)))

    def __repr__(self):
        return str(self).encode('utf-8')

    def append(self, *args):
        """Appends one or more strings to the grabber path."""
        self.path += ' '+' '.join(args)

    def command(self, *args):
        """
        Executed grabber command using one ore more additional strings.
        Returns unaltered response as a string
        """
        if self.path is '':
            return ''
        arg = ' '+' '.join(args)
        #print 'Running: %s %s' %(self.path, arg)
        fd = Popen('%s %s' % (self.path, arg), stdout=-1, stderr=-1,
                        shell=True)
        self.returncode = fd.wait()
        stdout,self.stderr = fd.communicate()

        if self.returncode:
            raise MythError('External grabber failed')
        return stdout

    
