Ticket #4600: mythtv_python_bindings.4.diff

File mythtv_python_bindings.4.diff, 19.6 KB (added by Hadley Rich <hads@…>, 16 years ago)
  • configure

     
    181181  echo ""
    182182  echo "  --with-bindings=LIST     install the bindings specified in the"
    183183  echo "                           comma-separated list"
    184   echo "                           Supported bindings: perl"
     184  echo "                           Supported bindings: perl, python"
    185185<<BLOCK_QUOTE
    186186  echo "  --disable-network        disable network support [default=no]"
    187187  echo "  --disable-ipv6           disable ipv6 support [default=no]"
     
    902902USING_LIST='
    903903    appleremote
    904904    bindings_perl
     905    bindings_python
    905906    darwin_da
    906907    dvdv
    907908    opengl
     
    11431144audio_jack_libs="-ljack"
    11441145enable audio_oss
    11451146bindings_perl="yes"
     1147bindings_python="yes"
    11461148dbox2="yes"
    11471149directfb="yes"
    11481150dvb_path="/usr/include"
     
    13691371          if test x"${binding}" = x"perl"; then
    13701372              bindings_perl="yes"
    13711373          fi
     1374          if test x"${binding}" = x"python"; then
     1375              bindings_python="yes"
     1376          fi
    13721377      done
    13731378  ;;
    13741379  --without-bindings=*)
     
    13771382          if test x"${binding}" = x"perl"; then
    13781383              bindings_perl="no"
    13791384          fi
     1385          if test x"${binding}" = x"python"; then
     1386              bindings_python="no"
     1387          fi
    13801388      done
    13811389  ;;
    13821390  --disable-encoders) disable $ENCODER_LIST
     
    16111619    disable ffserver
    16121620    disable v4l
    16131621    disable bindings_perl
     1622    disable bindings_python
    16141623    SLIBPREF="lib"
    16151624    SLIBSUF=".dll"
    16161625    EXESUF=".exe"
     
    29732982
    29742983echo "# Bindings"
    29752984echo "bindings_perl             ${bindings_perl-no}"
     2985echo "bindings_python           ${bindings_python-no}"
    29762986echo ""
    29772987
    29782988MYTH_CONFIG_H=libs/libmyth/mythconfig.h
  • libs/libmythtv/programinfo.h

     
    1616
    1717/* If NUMPROGRAMLINES gets updated following files need
    1818   updates and code changes:
    19    mythplugins/mythvideo/mythvideo/scripts/MythTV.py
    2019   mythplugins/mythweb/includes/mythbackend.php
    2120   mythplugins/mythweb/objects/MythTV.php
    2221   mythtv/bindings/perl/MythTV.pm
     22   mythtv/bindings/python/mythtv.py
    2323*/
    2424#define NUMPROGRAMLINES 46
    2525
  • libs/libmyth/mythcontext.h

     
    121121 *       mythtv/bindings/perl/MythTV.pm (version number)
    122122 *       mythtv/bindings/perl/MythTV/Program.pm (layout)
    123123 *
    124  *   MythVideo
    125  *      mythplugins/mythvideo/mythvideo/scripts/MythTV.py (version number)
    126  *      mythplugins/mythvideo/mythvideo/scripts/MythTV.py (layout)
     124 *   MythTV Python Bindings
     125 *       mythtv/bindings/python/mythtv.py (version number)
     126 *       mythtv/bindings/python/mythtv.py (layout)
    127127 */
    128128#define MYTH_PROTO_VERSION "39"
    129129
  • bindings/python/python.pro

     
     1include ( ../../config.mak )
     2
     3python_install.target = install
     4python_install.depends = all
     5python_install.commands = python setup.py install
     6
     7QMAKE_LINK=@-echo
     8QMAKE_EXTRA_UNIX_TARGETS += python_install
     9
  • 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"""
     4Provides classes for connecting to a MythTV backend.
     5
     6The MythTV class to handle connection to and querying of a MythTV backend.
     7The Recorder class representing and storing details of a tuner card.
     8The Program class for storing details of a TV program.
     9"""
     10
     11# vim:ts=4 sw=4 nowrap:
     12
     13import os
     14import sys
     15import socket
     16import shlex
     17import socket
     18import code
     19from datetime import datetime
     20
     21import mythdb
     22from mythlog import *
     23
     24log = MythLog(INFO, '%(levelname)s - %(message)s', 'mythtv')
     25
     26RECSTATUS = {
     27        'TunerBusy': -8,
     28        'LowDiskSpace': -7,
     29        'Cancelled': -6,
     30        'Deleted': -5,
     31        'Aborted': -4,
     32        'Recorded': -3,
     33        'Recording': -2,
     34        'WillRecord': -1,
     35        'Unknown': 0,
     36        'DontRecord': 1,
     37        'PreviousRecording': 2,
     38        'CurrentRecording': 3,
     39        'EarlierShowing': 4,
     40        'TooManyRecordings': 5,
     41        'NotListed': 6,
     42        'Conflict': 7,
     43        'LaterShowing': 8,
     44        'Repeat': 9,
     45        'Inactive': 10,
     46        'NeverRecord': 11,
     47}
     48
     49BACKEND_SEP = '[]:[]'
     50PROTO_VERSION = 39
     51PROGRAM_FIELDS = 46
     52
     53class MythTV:
     54        """
     55        A connection to a MythTV backend.
     56        """
     57        def __init__(self, conn_type='Monitor'):
     58                self.db = mythdb.MythDB(sys.argv[1:])
     59                self.master_host = self.db.getSetting('MasterServerIP')
     60                self.master_port = int(self.db.getSetting('MasterServerPort'))
     61               
     62                if not self.master_host:
     63                        log.Msg(CRITICAL, 'Unable to find MasterServerIP in database')
     64                        sys.exit(1)
     65                if not self.master_port:
     66                        log.Msg(CRITICAL, 'Unable to find MasterServerPort in database')
     67                        sys.exit(1)
     68               
     69                try:
     70                        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     71                        self.socket.settimeout(10)
     72                        self.socket.connect((self.master_host, self.master_port))
     73                        res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP)
     74                        if res[0] == 'REJECT':
     75                                log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
     76                                sys.exit(1)
     77                        res = self.backendCommand('ANN %s %s 0' % (conn_type, socket.gethostname()))
     78                        if res != 'OK':
     79                                log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
     80                        else:
     81                                log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port)
     82                except socket.error, e:
     83                        log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port)
     84                        sys.exit(1)
     85
     86        def backendCommand(self, data):
     87                """
     88                Sends a formatted command via a socket to the mythbackend.
     89
     90                Returns the result from the backend.
     91                """
     92                def recv():
     93                        """
     94                        Reads the data returned from the backend.
     95                        """
     96                        # The first 8 bytes of the response gives us the length
     97                        data = self.socket.recv(8)
     98                        try:
     99                                length = int(data)
     100                        except:
     101                                return ''
     102                        data = []
     103                        while length > 0:
     104                                chunk = self.socket.recv(length)
     105                                length = length - len(chunk)
     106                                data.append(chunk)
     107                        return ''.join(data)
     108               
     109                command = '%-8d%s' % (len(data), data)
     110                log.Msg(DEBUG, 'Sending command: %s', command)
     111                self.socket.send(command)
     112                return recv()
     113
     114        def getPendingRecordings(self):
     115                """
     116                Returns a list of Program objects which are scheduled to be recorded.
     117                """
     118                programs = []
     119                res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
     120                has_conflict = int(res.pop(0))
     121                num_progs = int(res.pop(0))
     122                log.Msg(DEBUG, '%s pending recordings', num_progs)
     123                for i in range(num_progs):
     124                        programs.append(
     125                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
     126                return programs
     127
     128        def getScheduledRecordings(self):
     129                """
     130                Returns a list of Program objects which are scheduled to be recorded.
     131                """
     132                programs = []
     133                res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
     134                num_progs = int(res.pop(0))
     135                log.Msg(DEBUG, '%s scheduled recordings', num_progs)
     136                for i in range(num_progs):
     137                        programs.append(
     138                                Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
     139                return programs
     140
     141        def getUpcomingRecordings(self):
     142                """
     143                Returns a list of Program objects which are scheduled to be recorded.
     144
     145                Sorts the list by recording start time and only returns those with
     146                record status of WillRecord.
     147                """
     148                def sort_programs_by_starttime(x, y):
     149                        if x.starttime > y.starttime:
     150                                return 1
     151                        elif x.starttime == y.starttime:
     152                                return 0
     153                        else:
     154                                return -1
     155                programs = []
     156                res = self.getPendingRecordings()
     157                for p in res:
     158                        if p.recstatus == RECSTATUS['WillRecord']:
     159                                programs.append(p)
     160                programs.sort(sort_programs_by_starttime)
     161                return programs
     162
     163        def getRecorderList(self):
     164                """
     165                Returns a list of recorders, or an empty list if none.
     166                """
     167                recorders = []
     168                c = self.db.cursor()
     169                c.execute('SELECT cardid FROM capturecard')
     170                row = c.fetchone()
     171                while row is not None:
     172                        recorders.append(int(row[0]))
     173                        row = c.fetchone()
     174                c.close()
     175                return recorders
     176
     177        def getFreeRecorderList(self):
     178                """
     179                Returns a list of free recorders, or an empty list if none.
     180                """
     181                res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
     182                recorders = [int(d) for d in res]
     183                return recorders
     184
     185        def getRecorderDetails(self, recorder_id):
     186                """
     187                Returns a Recorder object with details of the recorder.
     188                """
     189                c = self.db.cursor()
     190                c.execute("""SELECT cardid, cardtype, videodevice, hostname
     191                        FROM capturecard WHERE cardid = %s""", recorder_id)
     192                row = c.fetchone()
     193                if row:
     194                        recorder = Recorder(row)
     195                        return recorder
     196                else:
     197                        return None
     198
     199        def getCurrentRecording(self, recorder):
     200                """
     201                Returns a Program object for the current recorders recording.
     202                """
     203                res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder)
     204                return Program(res.split(BACKEND_SEP))
     205
     206        def isRecording(self, recorder):
     207                """
     208                Returns a boolean as to whether the given recorder is recording.
     209                """
     210                res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder)
     211                if res == '1':
     212                        return True
     213                else:
     214                        return False
     215
     216        def isActiveBackend(self, hostname):
     217                """
     218                Returns a boolean as to whether the given host is an active backend
     219                """
     220                res = self.backendCommand('QUERY_IS_ACTIVE_BACKEND[]:[]%s' % hostname)
     221                if res == 'TRUE':
     222                        return True
     223                else:
     224                        return False
     225
     226class Recorder:
     227        """
     228        Represents a MythTV capture card.
     229        """
     230        def __str__(self):
     231                return "Recorder %s (%s)" % (self.cardid, self.cardtype)
     232       
     233        def __repr__(self):
     234                return "Recorder %s (%s)" % (self.cardid, self.cardtype)
     235
     236        def __init__(self, data):
     237                """
     238                Load the list of data into the object.
     239                """
     240                self.cardid = data[0]
     241                self.cardtype = data[1]
     242                self.videodevice = data[2]
     243                self.hostname = data[3]
     244
     245class Program:
     246        """
     247        Represents a program with all the detail known.
     248        """
     249        def __str__(self):
     250                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     251
     252        def __repr__(self):
     253                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     254
     255        def __init__(self, data):
     256                """
     257                Load the list of data into the object.
     258                """
     259                self.title = data[0]
     260                self.subtitle = data[1]
     261                self.description = data[2]
     262                self.category = data[3]
     263                try:
     264                        self.chanid = int(data[4])
     265                except ValueError:
     266                        self.chanid = None
     267                self.channum = data[5] #chanstr
     268                self.callsign = data[6] #chansign
     269                self.channame = data[7]
     270                self.filename = data[8] #pathname
     271                self.fs_high = data[9]
     272                self.fs_low = data[10]
     273                self.starttime = datetime.fromtimestamp(int(data[11])) # startts
     274                self.endtime = datetime.fromtimestamp(int(data[12])) #endts
     275                self.duplicate = int(data[13])
     276                self.shareable = int(data[14])
     277                self.findid = int(data[15])
     278                self.hostname = data[16]
     279                self.sourceid = int(data[17])
     280                self.cardid = int(data[18])
     281                self.inputid = int(data[19])
     282                self.recpriority = int(data[20])
     283                self.recstatus = int(data[21])
     284                self.recordid = int(data[22])
     285                self.rectype = data[23]
     286                self.dupin = data[24]
     287                self.dupmethod = data[25]
     288                self.recstartts = datetime.fromtimestamp(int(data[26]))
     289                self.recendts = datetime.fromtimestamp(int(data[27]))
     290                self.repeat = int(data[28])
     291                self.programflags = data[29]
     292                self.recgroup = data[30]
     293                self.commfree = int(data[31])
     294                self.outputfilters = data[32]
     295                self.seriesid = data[33]
     296                self.programid = data[34]
     297                self.lastmodified = data[35]
     298                self.stars = float(data[36])
     299                self.airdate = data[37]
     300                self.hasairdate = int(data[38])
     301                self.playgroup = data[39]
     302                self.recpriority2 = int(data[40])
     303                self.parentid = data[41]
     304                self.storagegroup = data[42]
     305                self.audio_props = data[43]
     306                self.video_props = data[44]
     307                self.subtitle_type = data[45]
     308
     309if __name__ == '__main__':
     310        banner = '\'m\' is a MythTV instance.'
     311        try:
     312                import readline, rlcompleter
     313        except:
     314                pass
     315        else:
     316                readline.parse_and_bind("tab: complete")
     317                banner = banner + " TAB completion is available."
     318        m = MythTV()
     319        namespace = globals().copy()
     320        namespace.update(locals())
     321        code.InteractiveConsole(namespace).interact(banner)
  • bindings/python/mythtv/__init__.py

     
     1__all__ = ['mythtv', 'mythlog', 'mythdb', 'mythvideo']
     2
     3from mythlog import *
     4from mythdb import *
     5from mythtv import *
     6from mythvideo import *
  • bindings/python/mythtv/mythlog.py

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

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

     
    66using_bindings_perl {
    77    SUBDIRS+=perl
    88}
     9
     10using_bindings_python {
     11    SUBDIRS+=python
     12}