Ticket #4600: mythtv_python_bindings.diff

File mythtv_python_bindings.diff, 18.5 KB (added by Hadley Rich <hads@…>, 13 years ago)

hads propsed patch to put python bindings in mythtv/

  • bindings/python/setup.py

     
     1#!/usr/bin/python
     2
     3from distutils.core import setup
     4
     5setup(
     6        name='mythtv',
     7        version='0.21',
     8        description='MythTV Python bindings.',
     9        packages=['mythtv'],
     10)
  • bindings/python/mythtv/mythtv.py

     
     1#!/usr/bin/python
     2
     3# vim:ts=4 sw=4 nowrap:
     4
     5# system imports
     6import os
     7import sys
     8import socket
     9import shlex
     10import socket
     11import code
     12from datetime import datetime
     13# MythTV imports
     14from mythlog import *
     15import mythdb
     16
     17log = MythLog(INFO, '%(levelname)s - %(message)s', 'MythTV')
     18
     19RECSTATUS = {
     20        'TunerBusy': -8,
     21        'LowDiskSpace': -7,
     22        'Cancelled': -6,
     23        'Deleted': -5,
     24        'Aborted': -4,
     25        'Recorded': -3,
     26        'Recording': -2,
     27        'WillRecord': -1,
     28        'Unknown': 0,
     29        'DontRecord': 1,
     30        'PreviousRecording': 2,
     31        'CurrentRecording': 3,
     32        'EarlierShowing': 4,
     33        'TooManyRecordings': 5,
     34        'NotListed': 6,
     35        'Conflict': 7,
     36        'LaterShowing': 8,
     37        'Repeat': 9,
     38        'Inactive': 10,
     39        'NeverRecord': 11,
     40}
     41
     42BACKEND_SEP = '[]:[]'
     43PROTO_VERSION = 38
     44PROGRAM_FIELDS = 46
     45
     46class MythTV:
     47        """
     48        A connection to MythTV backend.
     49        """
     50        def __init__(self, conn_type='Monitor'):
     51                self.db = mythdb.MythDB(sys.argv[1:])
     52                self.master_host = self.db.getSetting('MasterServerIP')
     53                self.master_port = int(self.db.getSetting('MasterServerPort'))
     54               
     55                if not self.master_host:
     56                        log.Msg(CRITICAL, 'Unable to find MasterServerIP in database')
     57                        sys.exit(1)
     58                if not self.master_port:
     59                        log.Msg(CRITICAL, 'Unable to find MasterServerPort in database')
     60                        sys.exit(1)
     61               
     62                try:
     63                        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     64                        self.socket.settimeout(10)
     65                        self.socket.connect((self.master_host, self.master_port))
     66                        res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP)
     67                        if res[0] == 'REJECT':
     68                                log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
     69                                sys.exit(1)
     70                        res = self.backendCommand('ANN %s %s 0' % (conn_type, socket.gethostname()))
     71                        if res != 'OK':
     72                                log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
     73                        else:
     74                                log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port)
     75                except socket.error, e:
     76                        log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port)
     77                        sys.exit(1)
     78
     79        def backendCommand(self, data):
     80                """
     81                Sends a formatted command via a socket to the mythbackend.
     82
     83                Returns the result from the backend.
     84                """
     85                def recv():
     86                        """
     87                        Reads the data returned from the backend.
     88                        """
     89                        # The first 8 bytes of the response gives us the length
     90                        data = self.socket.recv(8)
     91                        try:
     92                                length = int(data)
     93                        except:
     94                                return ''
     95                        data = []
     96                        while length > 0:
     97                                chunk = self.socket.recv(length)
     98                                length = length - len(chunk)
     99                                data.append(chunk)
     100                        return ''.join(data)
     101               
     102                command = '%-8d%s' % (len(data), data)
     103                log.Msg(DEBUG, 'Sending command: %s', command)
     104                self.socket.send(command)
     105                return recv()
     106
     107        def getPendingRecordings(self):
     108                """
     109                Returns a list of Program objects which are scheduled to be recorded.
     110                """
     111                programs = []
     112                res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
     113                has_conflict = int(res.pop(0))
     114                num_progs = int(res.pop(0))
     115                log.Msg(DEBUG, '%s pending recordings', num_progs)
     116                for i in range(num_progs):
     117                        programs.append(
     118                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
     119                return programs
     120
     121        def getScheduledRecordings(self):
     122                """
     123                Returns a list of Program objects which are scheduled to be recorded.
     124                """
     125                programs = []
     126                res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
     127                num_progs = int(res.pop(0))
     128                log.Msg(DEBUG, '%s scheduled recordings', num_progs)
     129                for i in range(num_progs):
     130                        programs.append(
     131                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
     132                return programs
     133
     134        def getUpcomingRecordings(self):
     135                """
     136                Returns a list of Program objects which are scheduled to be recorded.
     137
     138                Sorts the list by recording start time and only returns those with
     139                record status of WillRecord.
     140                """
     141                def sort_programs_by_starttime(x, y):
     142                        if x.starttime > y.starttime:
     143                                return 1
     144                        elif x.starttime == y.starttime:
     145                                return 0
     146                        else:
     147                                return -1
     148                programs = []
     149                res = self.getPendingRecordings()
     150                for p in res:
     151                        if p.recstatus == RECSTATUS['WillRecord']:
     152                                programs.append(p)
     153                programs.sort(sort_programs_by_starttime)
     154                return programs
     155
     156        def getRecorderList(self):
     157                """
     158                Returns a list of recorders, or an empty list if none.
     159                """
     160                recorders = []
     161                c = self.db.cursor()
     162                c.execute('SELECT cardid FROM capturecard')
     163                row = c.fetchone()
     164                while row is not None:
     165                        recorders.append(int(row[0]))
     166                        row = c.fetchone()
     167                c.close()
     168                return recorders
     169
     170        def getFreeRecorderList(self):
     171                """
     172                Returns a list of free recorders, or an empty list if none.
     173                """
     174                res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
     175                recorders = [int(d) for d in res]
     176                return recorders
     177
     178        def getRecorderDetails(self, recorder_id):
     179                """
     180                Returns a Recorder object with details of the recorder.
     181                """
     182                c = self.db.cursor()
     183                c.execute("""SELECT cardid, cardtype, videodevice, hostname
     184                        FROM capturecard WHERE cardid = %s""", recorder_id)
     185                row = c.fetchone()
     186                if row:
     187                        recorder = Recorder(row)
     188                        return recorder
     189                else:
     190                        return None
     191
     192        def getCurrentRecording(self, recorder):
     193                """
     194                Returns a Program object for the current recorders recording.
     195                """
     196                res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder)
     197                return Program(res.split(BACKEND_SEP))
     198
     199        def isRecording(self, recorder):
     200                """
     201                Returns a boolean as to whether the given recorder is recording.
     202                """
     203                res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder)
     204                if res == '1':
     205                        return True
     206                else:
     207                        return False
     208
     209        def isActiveBackend(self, hostname):
     210                """
     211                Returns a boolean as to whether the given host is an active backend
     212                """
     213                res = self.backendCommand('QUERY_IS_ACTIVE_BACKEND[]:[]%s' % hostname)
     214                if res == 'TRUE':
     215                        return True
     216                else:
     217                        return False
     218
     219class MythVideo:
     220        def __init__(self):
     221                self.db = MythDB()
     222
     223        def pruneMetadata(self):
     224                """
     225                Removes metadata from the database for files that no longer exist.
     226                """
     227                c = self.db.cursor()
     228                c.execute("""
     229                        SELECT intid, filename
     230                        FROM videometadata""")
     231               
     232                row = c.fetchone()
     233                while row is not None:
     234                        intid = row[0]
     235                        filename = row[1]
     236                        if not os.path.exists(filename):
     237                                log.Msg(INFO, '%s not exist, removing metadata...', filename)
     238                                c2 = self.db.cursor()
     239                                c2.execute("""DELETE FROM videometadata WHERE intid = %s""", (intid,))
     240                                c2.close()
     241                        row = c.fetchone()
     242                c.close()
     243
     244        def getGenreId(self, genre_name):
     245                """
     246                Find the id of the given genre from MythDB.
     247               
     248                If the genre does not exist, insert it and return its id.
     249                """
     250                c = self.db.cursor()
     251                c.execute("SELECT intid FROM videocategory WHERE lower(category) = %s", (genre_name,))
     252                row = c.fetchone()
     253                c.close()
     254               
     255                if row is not None:
     256                        return row[0]
     257               
     258                # Insert a new genre.
     259                c = self.db.cursor()
     260                c.execute("INSERT INTO videocategory(category) VALUES (%s)", (genre_name.capitalize(),))
     261                newid = c.lastrowid
     262                c.close()
     263               
     264                return newid
     265
     266        def getMetadataId(self, videopath):
     267                """
     268                Finds the MythVideo metadata id for the given video path from the MythDB, if any.
     269               
     270                Returns None if no metadata was found.
     271                """
     272                c = self.db.cursor()
     273                c.execute("""
     274                        SELECT intid
     275                        FROM videometadata
     276                        WHERE filename = %s""", (videopath,))
     277                row = c.fetchone()
     278                c.close()
     279               
     280                if row is not None:
     281                        return row[0]
     282                else:
     283                        return None
     284
     285        def hasMetadata(self, videopath):
     286                """
     287                Determines if the given videopath has any metadata in the DB
     288               
     289                Returns False if no metadata was found.
     290                """
     291                c = self.db.cursor()
     292                c.execute("""
     293                        SELECT category, year
     294                        FROM videometadata
     295                        WHERE filename = %s""", (videopath,))
     296                row = c.fetchone()
     297                c.close()
     298               
     299                if row is not None:
     300                        # If category is 0 and year is 1895, we can safely assume no metadata
     301                        if (row[0] == 0) and (row[1] == 1895):
     302                                return False
     303                        else:
     304                                return True
     305                else:
     306                        return False
     307
     308        def getMetadata(self, id):
     309                """
     310                Finds the MythVideo metadata for the given id from the MythDB, if any.
     311               
     312                Returns None if no metadata was found.
     313                """
     314                c = self.db.cursor()
     315                c.execute("""
     316                        SELECT *
     317                        FROM videometadata
     318                        WHERE intid = %s""", (id,))
     319                row = c.fetchone()
     320                c.close()
     321               
     322                if row is not None:
     323                        return row
     324                else:
     325                        return None
     326
     327        def setMetadata(self, data, id=None):
     328                """
     329                Adds or updates the metadata in the database for a video item.
     330                """
     331                c = self.db.cursor()
     332                if id is None:
     333                        fields = ', '.join(data.keys())
     334                        format_string = ', '.join(['%s' for d in data.values()])
     335                        sql = "INSERT INTO videometadata(%s) VALUES(%s)" % (fields, format_string)
     336                        c.execute(sql, data.values())
     337                        intid = c.lastrowid
     338                        c.close()
     339                        return intid
     340                else:
     341                        log.Msg(DEBUG, 'Updating metadata for %s', id)
     342                        format_string = ', '.join(['%s = %%s' % d for d in data])
     343                        sql = "UPDATE videometadata SET %s WHERE intid = %%s" % format_string
     344                        sql_values = data.values()
     345                        sql_values.append(id)
     346                        c.execute(sql, sql_values)
     347                        c.close()
     348
     349class Recorder:
     350        def __str__(self):
     351                return "Recorder %s (%s)" % (self.cardid, self.cardtype)
     352       
     353        def __repr__(self):
     354                return "Recorder %s (%s)" % (self.cardid, self.cardtype)
     355
     356        def __init__(self, data):
     357                """
     358                Load the list of data into the object.
     359                """
     360                self.cardid = data[0]
     361                self.cardtype = data[1]
     362                self.videodevice = data[2]
     363                self.hostname = data[3]
     364
     365class Program:
     366        def __str__(self):
     367                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     368
     369        def __repr__(self):
     370                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     371
     372        def __init__(self, data):
     373                """
     374                Load the list of data into the object.
     375                """
     376                self.title = data[0]
     377                self.subtitle = data[1]
     378                self.description = data[2]
     379                self.category = data[3]
     380                try:
     381                        self.chanid = int(data[4])
     382                except ValueError:
     383                        self.chanid = None
     384                self.channum = data[5] #chanstr
     385                self.callsign = data[6] #chansign
     386                self.channame = data[7]
     387                self.filename = data[8] #pathname
     388                self.fs_high = data[9]
     389                self.fs_low = data[10]
     390                self.starttime = datetime.fromtimestamp(int(data[11])) # startts
     391                self.endtime = datetime.fromtimestamp(int(data[12])) #endts
     392                self.duplicate = int(data[13])
     393                self.shareable = int(data[14])
     394                self.findid = int(data[15])
     395                self.hostname = data[16]
     396                self.sourceid = int(data[17])
     397                self.cardid = int(data[18])
     398                self.inputid = int(data[19])
     399                self.recpriority = int(data[20])
     400                self.recstatus = int(data[21])
     401                self.recordid = int(data[22])
     402                self.rectype = data[23]
     403                self.dupin = data[24]
     404                self.dupmethod = data[25]
     405                self.recstartts = datetime.fromtimestamp(int(data[26]))
     406                self.recendts = datetime.fromtimestamp(int(data[27]))
     407                self.repeat = int(data[28])
     408                self.programflags = data[29]
     409                self.recgroup = data[30]
     410                self.commfree = int(data[31])
     411                self.outputfilters = data[32]
     412                self.seriesid = data[33]
     413                self.programid = data[34]
     414                self.lastmodified = data[35]
     415                self.stars = float(data[36])
     416                self.airdate = data[37]
     417                self.hasairdate = int(data[38])
     418                self.playgroup = data[39]
     419                self.recpriority2 = int(data[40])
     420                self.parentid = data[41]
     421                self.storagegroup = data[42]
     422                self.audio_props = data[43]
     423                self.video_props = data[44]
     424                self.subtitle_type = data[45]
     425
     426if __name__ == '__main__':
     427        banner = '\'m\' is a MythTV instance.'
     428        try:
     429                import readline, rlcompleter
     430        except:
     431                pass
     432        else:
     433                readline.parse_and_bind("tab: complete")
     434                banner = banner + " TAB completion is available."
     435        m = MythTV()
     436        namespace = globals().copy()
     437        namespace.update(locals())
     438        code.InteractiveConsole(namespace).interact(banner)
  • bindings/python/mythtv/mythlog.py

     
     1#!/usr/bin/python
     2
     3# vim:ts=4 sw=4 nowrap:
     4
     5# MythTV imports
     6import logging
     7
     8# Vars to imporove readability
     9# (There are some duplicates here... should they be removed?)
     10CRITICAL = logging.CRITICAL
     11FATAL = logging.FATAL
     12ERROR = logging.ERROR
     13WARNING = logging.WARNING
     14WARN = logging.WARN
     15INFO = logging.INFO
     16DEBUG = logging.DEBUG
     17
     18class MythLog:
     19        """
     20        A simple logging class
     21        """
     22        def __init__(self, level, format, instance):
     23                self.log = logging.getLogger(instance)
     24                self.log.setLevel(level)
     25                self.ch = logging.StreamHandler()
     26                self.ch.setFormatter(logging.Formatter(format))
     27                self.log.addHandler(self.ch)
     28
     29        def Msg(self, level, msg, *args, **kwargs):
     30                self.log.log(level, msg, *args, **kwargs)
     31
     32class MythError:
     33        """
     34        A simple exception class
     35        """
     36        def __init__(self, message):
     37                self.message = message
     38
     39        def __repr__(self):
     40                print ': ' + self.message
     41
     42if __name__ == '__main__':
     43        print 'MythLog can only be used as a module'
  • bindings/python/mythtv/mythdb.py

     
     1#!/usr/bin/python
     2
     3# vim:ts=4 sw=4 nowrap:
     4
     5# system imports
     6import os
     7import sys
     8import shlex
     9import code
     10import getopt
     11from datetime import datetime
     12# MythTV imports
     13from mythlog import *
     14
     15# create logging object
     16log = MythLog(INFO, '%(levelname)s - %(message)s', 'MythDB')
     17
     18# check for dependency
     19try:
     20        import MySQLdb
     21except:
     22        log.Msg(CRITICAL, "MySQLdb (python-mysqldb) is required but is not found.")
     23        sys.exit(1)
     24
     25class MythDB:
     26        """
     27        A connection to the mythtv database.
     28        """
     29        def __init__(self, args):
     30                # Setup connection variables
     31                dbconn = {
     32                        'host'  : None,
     33                        'name'  : None,
     34                        'user'  : None,
     35                        'pass'  : None
     36                }
     37               
     38                # Try to read the mysql.txt file used by MythTV.
     39                # Order taken from libs/libmyth/mythcontext.cpp
     40                config_files = [
     41                        '/usr/local/share/mythtv/mysql.txt',
     42                        '/usr/share/mythtv/mysql.txt',
     43                        '/usr/local/etc/mythtv/mysql.txt',
     44                        '/etc/mythtv/mysql.txt',
     45                        os.path.expanduser('~/.mythtv/mysql.txt'),
     46                ]
     47                if 'MYTHCONFDIR' in os.environ:
     48                        config_locations.append('%s/mysql.txt' % os.environ['MYTHCONFDIR'])
     49               
     50                found_config = False
     51                for config_file in config_files:
     52                        try:
     53                                config = shlex.shlex(open(config_file))
     54                                config.wordchars += "."
     55                        except:
     56                                continue
     57       
     58                        dbconn['host'] = None
     59                        dbconn['name'] = None
     60                        dbconn['user'] = None
     61                        dbconn['pass'] = None
     62                        token = config.get_token()
     63                        while  token != config.eof and not found_config:
     64                                if token == "DBHostName":
     65                                        if config.get_token() == "=":
     66                                                dbconn['host'] = config.get_token()
     67                                elif token == "DBName":
     68                                        if config.get_token() == "=":
     69                                                dbconn['name'] = config.get_token()
     70                                elif token == "DBUserName":
     71                                        if config.get_token() == "=":
     72                                                dbconn['user'] = config.get_token()
     73                                elif token == "DBPassword":
     74                                        if config.get_token() == "=":
     75                                                dbconn['pass'] = config.get_token()
     76                                token = config.get_token()
     77                        if dbconn['host'] != None and dbconn['name'] != None and dbconn['user'] != None and dbconn['pass'] != None:
     78                                log.Msg(INFO, 'Using config %s', config_file)
     79                                found_config = True
     80                                break
     81
     82                # Overrides from command line parameters
     83                try:
     84                opts, args = getopt.getopt(args, '', ['dbhost=', 'user=', 'pass=', 'database='])
     85                        for o, a in opts:
     86                                if o == '--dbhost':
     87                                        dbconn['host'] = a
     88                                if o == '--user':
     89                                        dbconn['user'] = a
     90                                if o == '--pass':
     91                                        dbconn['pass'] = a
     92                                if o == '--database':
     93                                        dbconn['name'] = a
     94        except:
     95                        pass
     96
     97                if not dbconn['host'] and not found_config:
     98                        raise MythError('Unable to find MythTV configuration file')
     99
     100                try:
     101                        self.db = MySQLdb.connect(user=dbconn['user'], host=dbconn['host'], passwd=dbconn['pass'], db=dbconn['name'])
     102                        log.Msg(INFO, 'DB Connection info (host:%s, name:%s, user:%s, pass:%s)', dbconn['host'], dbconn['name'], dbconn['user'], dbconn['pass'])
     103                except:
     104                        raise MythError('Connection failed for \'%s\'@\'%s\' to database %s using password %s' % (dbconn['user'], dbconn['host'], dbconn['name'], dbconn['pass']))
     105
     106        def getAllSettings(self, hostname=None):
     107                """
     108                Returns values for all settings.
     109               
     110                Returns None if there are no settings. If multiple rows are
     111                found (multiple hostnames), returns the value of the first one.
     112                """
     113                log.Msg(DEBUG, 'Retrieving all setting for host %s', hostname)
     114                c = self.db.cursor()
     115                if hostname is None:
     116                        c.execute("""
     117                                SELECT value, data
     118                                FROM settings
     119                                WHERE hostname IS NULL""")
     120                else:
     121                        c.execute("""
     122                                SELECT value, data
     123                                FROM settings
     124                                WHERE hostname LIKE('%s%%')""" %
     125                                (hostname)
     126                        )
     127                rows = c.fetchall()
     128                c.close()
     129               
     130                if rows:
     131                        return rows
     132                else:
     133                        return None
     134
     135        def getSetting(self, value, hostname=None):
     136                """
     137                Returns the value for the given MythTV setting.
     138               
     139                Returns None if the setting was not found. If multiple rows are
     140                found (multiple hostnames), returns the value of the first one.
     141                """
     142                log.Msg(DEBUG, 'Looking for setting %s for host %s', value, hostname)
     143                c = self.db.cursor()
     144                if hostname is None:
     145                        c.execute("""
     146                                SELECT data
     147                                FROM settings
     148                                WHERE value LIKE('%s') AND hostname IS NULL LIMIT 1""" %
     149                                (value))
     150                else:
     151                        c.execute("""
     152                                SELECT data
     153                                FROM settings
     154                                WHERE value LIKE('%s') AND hostname LIKE('%s%%') LIMIT 1""" %
     155                                (value, hostname))
     156                row = c.fetchone()
     157                c.close()
     158               
     159                if row:
     160                        return row[0]
     161                else:
     162                        return None
     163
     164        def cursor(self):
     165                return self.db.cursor()
     166
     167if __name__ == '__main__':
     168        banner = "'mdb' is a MythDB instance."
     169        try:
     170                import readline, rlcompleter
     171        except:
     172                pass
     173        else:
     174                readline.parse_and_bind("tab: complete")
     175                banner = banner + " TAB completion is available."
     176        mdb = MythDB(sys.argv[1:])
     177        namespace = globals().copy()
     178        namespace.update(locals())
     179        code.InteractiveConsole(namespace).interact(banner)