Ticket #4600: mythtv_python_bindings.3.diff

File mythtv_python_bindings.3.diff, 19.6 KB (added by trisooma <trisooma@…>, 16 years ago)

version 3

  • python/interactive_mythtv.py

     
     1#!/usr/bin/python
     2
     3# vim:ts=4 sw=4 nowrap:
     4
     5# version:              0.1
     6# date:                 2008-02-06
     7# by:                   trisooma
     8# description:  This file is intended to be used by developers who wish to use the python
     9#                               bindings in an interactive fashion
     10
     11# system imports
     12import sys
     13import code
     14# package imports
     15import mythtv
     16
     17# create logging object
     18log = mythtv.Log(mythtv.INFO, '%(levelname)s - %(message)s', 'functions')
     19
     20if __name__ == '__main__':
     21        try:
     22                mb = mythtv.Base()
     23        except mythtv.Error, e:
     24                log.Msg(mythtv.WARN, 'Exception occurred during initialization of mythtv.Base: %s', e.message)
     25                sys.exit(1)
     26
     27        try:
     28                import readline, rlcompleter
     29        except:
     30                pass
     31        else:
     32                readline.parse_and_bind('tab:complete')
     33                banner = '\'mb\' is an instance of mythtv.Base()\n'
     34                banner = banner + 'TAB completion is available.'
     35                namespace = globals().copy()
     36                namespace.update(locals())
     37                code.InteractiveConsole(namespace).interact(banner)
  • python/mythtv/base.py

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

     
     1__all__ = ['base', 'db', 'log']
     2
     3from log import *
     4from base import *
     5from db import *
     6
  • python/mythtv/log.py

     
     1#!/usr/bin/python
     2
     3# vim:ts=4 sw=4 nowrap:
     4
     5# version:      0.1
     6# date:         2008-02-06
     7# by:           trisooma
     8# description:  Part of mythtv python bindings. These classes can be used for
     9#                               logging and raising exceptions
     10
     11# package deps
     12import logging
     13
     14# Vars to imporove readability
     15# (There are some duplicates here... should they be removed?)
     16CRITICAL = logging.CRITICAL
     17FATAL = logging.FATAL
     18ERROR = logging.ERROR
     19WARNING = logging.WARNING
     20WARN = logging.WARN
     21INFO = logging.INFO
     22DEBUG = logging.DEBUG
     23
     24class Log:
     25        """
     26        A simple logging class
     27        """
     28        def __init__(self, level, format, instance):
     29                self.log = logging.getLogger(instance)
     30                self.log.setLevel(level)
     31                self.ch = logging.StreamHandler()
     32                self.ch.setFormatter(logging.Formatter(format))
     33                self.log.addHandler(self.ch)
     34
     35        def Msg(self, level, msg, *args, **kwargs):
     36                self.log.log(level, msg, *args, **kwargs)
     37
     38class Error:
     39        """
     40        A simple exception class
     41        """
     42        def __init__(self, message):
     43                self.message = message
     44
     45        def __repr__(self):
     46                print ': ' + self.message
     47
     48        def __str__(self):
     49                print ': ' + self.message
     50
     51if __name__ == '__main__':
     52        print 'Log and Error are now part of the mythtv package'
  • python/mythtv/db.py

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