Ticket #6885: pyth.updates10.patch

File pyth.updates10.patch, 34.8 KB (added by Raymond Wagner <raymond@…>, 15 years ago)
  • MythTV/MythVideo.py

     
    99
    1010log = MythLog(CRITICAL, '#%(levelname)s - %(message)s', 'MythVideo')
    1111
    12 class MythVideo:
     12MVSCHEMA_VERSION = 1028
     13
     14class MythVideo(object):
    1315        """
    1416        Provides convinience methods to access the MythTV MythVideo database.
    1517        """
     
    1921                """
    2022                self.db = MythDB()
    2123
     24                # check schema version
     25                sver = int(self.db.getSetting('mythvideo.DBSchemaVer'))
     26                if MVSCHEMA_VERSION != sver:
     27                        log.Msg(CRITICAL, 'DB speaks schema version %d, but we speak version %d',
     28                                        sver, MVSCHEMA_VERSION)
     29                        raise MythError('Mismatched schema version')
     30
    2231        def rtnVideoStorageGroup(self, host=None):
    2332                '''Get the storage group 'Videos' directory for the suppied host name or default to the localhost.
    2433                return None if no Videos Storage Group found
     
    2736                if not host: # If a hostname was not supplied then use the local host name
    2837                        host = gethostname()
    2938
    30                 # Get storagegroup table field names
    31                 table_names = self.getTableFieldNames(u'storagegroup')
     39                ret = []
     40                for i in self.db.getStorageGroup('Videos',host):
     41                        ret.append(i['dirname'])
    3242
    33                 cur = self.db.cursor()
    34                 # Check is there are storage groups for the supplied host or default to the local host
    35                 try:
    36                         cur.execute(u"select * from storagegroup")
    37                 except MySQLdb.Error, e:
    38                         log.Msg(INFO, u"! Error: Reading storagegroup MythTV table: %d: %s\n" % (e.args[0], e.args[1]))
     43                if ret:
     44                        return ret
     45                else:
    3946                        return None
    4047
    41                 videos_dir = []
    42                 while True:
    43                         data_id = cur.fetchone()
    44                         if not data_id:
    45                                 break
    46                         record = {}
    47                         i = 0
    48                         for elem in data_id:
    49                                 if table_names[i] == 'groupname' or table_names[i] == 'hostname' or table_names[i] == 'dirname':
    50                                         record[table_names[i]] = elem
    51                                 i+=1
    52                         if record['hostname'].lower() == host.lower() and record['groupname'] == u'Videos':
    53                                 # Add a slash if mussing to any storage group dirname
    54                                 if record['dirname'][-1:] == '/':
    55                                         videos_dir.append(record['dirname'])
    56                                 else:
    57                                         videos_dir.append(record['dirname']+u'/')
    58                         continue
    59                 cur.close()
    60 
    61                 if not len(videos_dir):
    62                         return None
    63 
    64                 return videos_dir
    65         # end getStorageGroups
    66 
    6748        def pruneMetadata(self):
    6849                """
    6950                Removes metadata from the database for files that no longer exist.
     
    11596                This function should have been called getCategoryId
    11697                """
    11798                c = self.db.cursor()
    118                 c.execute("SELECT intid FROM videocategory WHERE lower(category) = %s", (genre_name,))
     99                c.execute("""SELECT intid FROM videocategory
     100                                WHERE lower(category) = %s""", (genre_name,))
    119101                row = c.fetchone()
    120102                c.close()
    121103
     
    124106
    125107                # Insert a new genre.
    126108                c = self.db.cursor()
    127                 c.execute("INSERT INTO videocategory(category) VALUES (%s)", (genre_name.capitalize(),))
     109                c.execute("""INSERT INTO videocategory(category)
     110                                VALUES (%s)""", (genre_name.capitalize(),))
    128111                newid = c.lastrowid
    129112                c.close()
    130113
  • MythTV/MythTV.py

     
    4949PROTO_VERSION = 50
    5050PROGRAM_FIELDS = 47
    5151
    52 class MythTV:
     52class MythTV(object):
    5353        """
    5454        A connection to a MythTV backend.
    5555        """
    56         def __init__(self, conn_type='Monitor'):
     56
     57        locked_tuners = []
     58
     59        def __init__(self, backend='', conn_type='Monitor'):
    5760                self.db = MythDB(sys.argv[1:])
    58                 self.master_host = self.db.getSetting('MasterServerIP')
    59                 self.master_port = int(self.db.getSetting('MasterServerPort'))
    6061
    61                 if not self.master_host:
     62                if len(backend) == 0:  # use master backend
     63                        self.host = self.db.getSetting('MasterServerIP')
     64                        self.port = self.db.getSetting('MasterServerPort')
     65                elif re.match('(?:\d{1,3}\.){3}\d{1,3}',backend): # given an ip address
     66                        self.host = backend
     67                        c = self.db.cursor()
     68                        c.execute("""SELECT hostname FROM settings WHERE
     69                                        value='BackendServerIP' AND
     70                                        data=%s""", self.host)
     71                        backend = c.fetchone()[0]
     72                        self.port = self.db.getSetting('BackendServerPort',backend)
     73                        c.close()
     74                else: # assume given a hostname
     75                        self.host = self.db.getSetting('BackendServerIP',backend)
     76                        if not self.host: # try a truncated hostname
     77                                backend = backend.split('.')[0]
     78                                self.host = self.db.getSetting('BackendServerIP',backend)
     79                        self.port = self.db.getSetting('BackendServerPort',backend)
     80                       
     81                if not self.host:
    6282                        log.Msg(CRITICAL, 'Unable to find MasterServerIP in database')
    6383                        sys.exit(1)
    64                 if not self.master_port:
     84                if not self.port:
    6585                        log.Msg(CRITICAL, 'Unable to find MasterServerPort in database')
    6686                        sys.exit(1)
     87                self.port = int(self.port)
    6788
    6889                try:
    6990                        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    7091                        self.socket.settimeout(10)
    71                         self.socket.connect((self.master_host, self.master_port))
     92                        self.socket.connect((self.host, self.port))
    7293                        res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP)
    7394                        if res[0] == 'REJECT':
    7495                                log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
     
    7798                        if res != 'OK':
    7899                                log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
    79100                        else:
    80                                 log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port)
     101                                log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.host, self.port)
    81102                except socket.error, e:
    82                         log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port)
     103                        log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.host, self.port)
    83104                        sys.exit(1)
    84105
    85106        def __del__(self):
     107                self.freeTuner()
    86108                self.backendCommand('DONE')
    87109                self.socket.shutdown(1)
    88110                self.socket.close()
    89111
     112        def close(self):
     113                self.__del__()
     114
    90115        def backendCommand(self, data):
    91116                """
    92117                Sends a formatted command via a socket to the mythbackend.
     
    203228                else:
    204229                        return None
    205230
     231        def lockTuner(self,id=None):
     232                """
     233                Request a tuner be locked from use, optionally specifying which tuner
     234                Returns a tuple of ID, video device node, audio device node, vbi device node
     235                Returns an ID of -2 if tuner is locked, or -1 if no tuner could be found
     236                """
     237                local = True
     238                cmd = 'LOCK_TUNER'
     239                if id is not None:
     240                        cmd += ' %d' % id
     241                        res = self.getRecorderDetails(id).hostname
     242                        if res != socket.gethostname():
     243                                local = False
     244
     245                res = ''
     246                if local:
     247                        res = self.backendCommand(cmd).split(BACKEND_SEP)
     248                else:
     249                        myth = MythTV(res)
     250                        res = myth.backendCommand(cmd).split(BACKEND_SEP)
     251                        myth.close()
     252                res[0] = int(res[0])
     253                if res[0] > 0:
     254                        self.locked_tuners.append(res[0])
     255                return tuple(res)
     256               
     257
     258        def freeTuner(self,id=None):
     259                """
     260                Frees a requested tuner ID
     261                If no ID given, free all tuners listed as used by this class instance
     262                """
     263                def free(self,id):
     264                        res = self.getRecorderDetails(id).hostname
     265                        if res == socket.gethostname():
     266                                self.backendCommand('FREE_TUNER %d' % id)
     267                        else:
     268                                myth = MythTV(res)
     269                                myth.backendCommand('FREE_TUNER %d' % id)
     270                                myth.close()
     271
     272                if id is None:
     273                        for i in range(len(self.locked_tuners)):
     274                                free(self,self.locked_tuners.pop())
     275                else:
     276                        try:
     277                                self.locked_tuners.remove(id)
     278                        except:
     279                                pass
     280                        free(self,id)   
     281
    206282        def getCurrentRecording(self, recorder):
    207283                """
    208284                Returns a Program object for the current recorders recording.
    209285                """
    210                 res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder)
     286                res = self.backendCommand('QUERY_RECORDER '+BACKEND_SEP.join([recorder,'GET_CURRENT_RECORDING']))
    211287                return Program(res.split(BACKEND_SEP))
    212288
    213289        def isRecording(self, recorder):
    214290                """
    215291                Returns a boolean as to whether the given recorder is recording.
    216292                """
    217                 res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder)
     293                res = self.backendCommand('QUERY_RECORDER '+BACKEND_SEP.join([recorder,'IS_RECORDING']))
    218294                if res == '1':
    219295                        return True
    220296                else:
     
    224300                """
    225301                Returns a boolean as to whether the given host is an active backend
    226302                """
    227                 res = self.backendCommand('QUERY_IS_ACTIVE_BACKEND[]:[]%s' % hostname)
     303                res = self.backendCommand(BACKEND_SEP.join(['QUERY_IS_ACTIVE_BACKEND',hostname]))
    228304                if res == 'TRUE':
    229305                        return True
    230306                else:
     
    245321                Returns a list of all Program objects which have already recorded
    246322                """
    247323                programs = []
    248                 res = self.backendCommand('QUERY_RECORDINGS Play').split('[]:[]')
     324                res = self.backendCommand('QUERY_RECORDINGS Play').split(BACKEND_SEP)
    249325                num_progs = int(res.pop(0))
    250326                log.Msg(DEBUG, '%s total recordings', num_progs)
    251327                for i in range(num_progs):
     
    253329                                + PROGRAM_FIELDS]))
    254330                return tuple(programs)
    255331
     332        def getExpiring(self):
     333                """
     334                Returns a tuple of all Program objects nearing expiration
     335                """
     336                programs = []
     337                res = self.backendCommand('QUERY_GETEXPIRING').split(BACKEND_SEP)
     338                num_progs = int(res.pop(0))
     339                for i in range(num_progs):
     340                        programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
     341                                + PROGRAM_FIELDS]))
     342                return tuple(programs)
     343
    256344        def getCheckfile(self,program):
    257345                """
    258346                Returns location of recording in file system
    259347                """
    260                 res = self.backendCommand('QUERY_CHECKFILE[]:[]1[]:[]%s' % program.toString()).split(BACKEND_SEP)
     348                res = self.backendCommand(BACKEND_SEP.join(['QUERY_CHECKFILE','1',program.toString()])).split(BACKEND_SEP)
    261349                if res[0] == 0:
    262350                        return None
    263351                else:
     
    271359                command = 'DELETE_RECORDING'
    272360                if force:
    273361                        command = 'FORCE_DELETE_RECORDING'
    274                 return self.backendCommand('%s%s%s' % (command,BACKEND_SEP,program.toString()))
     362                return self.backendCommand(BACKEND_SEP.join([command,program.toString()]))
    275363
    276364        def forgetRecording(self,program):
    277365                """
    278366                Forgets old recording and allows it to be re-recorded
    279367                """
    280                 self.backendCommand('FORGET_RECORDING%s%s' % (BACKEND_SEP,program.toString()))
     368                self.backendCommand(BACKEND_SEP.join(['FORGET_RECORDING',program.toString()]))
    281369
    282370        def deleteFile(self,file,sgroup):
    283371                """
    284372                Deletes a file from specified storage group on the connected backend
    285373                Takes a relative file path from the root of the storage group, and returns 1 on success
    286374                """
    287                 return self.backendCommand('DELETE_FILE%s%s%s%s' % (BACKEND_SEP,file,BACKEND_SEP,sgroup))
     375                return self.backendCommand(BACKEND_SEP.join(['DELETE_FILE',file,sgroup]))
    288376
    289377        def getFreeSpace(self,all=False):
    290378                """
     
    329417                res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP)
    330418                return (float(res[0]),float(res[1]),float(res[2]))
    331419
     420        def getUptime(self):
     421                """
     422                Returns machine uptime in seconds
     423                """
     424                return self.backendCommand('QUERY_UPTIME')
     425
    332426        def getSGList(self,host,sg,path):
    333427                """
    334428                Returns a tuple of directories and files
    335429                """
    336                 res = self.backendCommand('QUERY_SG_GETFILELIST%s%s%s%s%s%s' % (BACKEND_SEP,host,BACKEND_SEP,sg,BACKEND_SEP,path)).split(BACKEND_SEP)
     430                res = self.backendCommand(BACKEND_SEP.join(['QUERY_SG_GETFILELIST',host,sg,path])).split(BACKEND_SEP)
    337431                if res[0] == 'EMPTY LIST':
    338432                        return -1
    339433                if res[0] == 'SLAVE UNREACHABLE: ':
     
    352446                """
    353447                Returns a tuple of last modification time and file size
    354448                """
    355                 res = self.backendCommand('QUERY_SG_FILEQUERY%s%s%s%s%s%s' % (BACKEND_SEP,host,BACKEND_SEP,sg,BACKEND_SEP,path)).split(BACKEND_SEP)
     449                res = self.backendCommand(BACKEND_SEP.join(['QUERY_SG_FILEQUERY',host,sg,path])).split(BACKEND_SEP)
    356450                if res[0] == 'EMPTY LIST':
    357451                        return -1
    358452                if res[0] == 'SLAVE UNREACHABLE: ':
     
    382476                port = self.db.getSetting("NetworkControlPort",host)
    383477                return Frontend(host,port)
    384478
     479        def getLastGuideData(self):
     480                """
     481                Returns the last dat for which guide data is available
     482                On error, 0000-00-00 00:00 is returned
     483                """
     484                return self.backendCommand('QUERY_GUIDEDATATHROUGH')
     485
    385486        def joinInt(self,high,low):
    386487                """
    387488                Returns a single long from a pair of signed integers
     
    394495                """
    395496                return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32
    396497
    397 class FileTransfer:
     498class FileTransfer(object):
    398499        """
    399500        A connection to mythbackend intended for file transfers
    400501        """
     
    458559                        if res[0] == 'REJECT':
    459560                                log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
    460561                                sys.exit(1)
    461                         res = self.send('ANN FileTransfer %s %d %d %d%s%s%s%s' % (socket.gethostname(), write, False, -1, BACKEND_SEP, self.filename, BACKEND_SEP, self.sgroup))
     562                        res = self.send('ANN FileTransfer %s %d %d %s' % (socket.gethostbyname(),write, False, BACKEND_SEP.join(['-1',self.filename,self.sgroup])))
    462563                        if res.split(BACKEND_SEP)[0] != 'OK':
    463564                                log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
    464565                        else:
     
    474575
    475576        def __del__(self):
    476577                if self.sockno:
    477                         self.comsock.backendCommand('QUERY_FILETRANSFER %d%sDONE' % (self.sockno, BACKEND_SEP))
     578                        self.comsock.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP.join([str(self.sockno), 'JOIN']))
    478579                if self.datsock:
    479580                        self.datsock.shutdown(1)
    480581                        self.datsock.close()
     
    533634                        csize = self.tsize
    534635                        rsize = size - csize
    535636                       
    536                 res = self.comsock.backendCommand('QUERY_FILETRANSFER %d%sREQUEST_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,csize))
     637                res = self.comsock.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP.join([str(self.sockno),'REQUEST_BLOCK',str(csize)]))
    537638                self.pos += int(res)
    538639#               if int(res) == csize:
    539640#                       if csize < size:
     
    561662                        buff = data[size:]
    562663                        data = data[:size]
    563664                self.pos += int(self.datsock.send(data))
    564                 self.comsock.backendCommand('QUERY_FILETRANSFER %d%sWRITE_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,size))
     665                self.comsock.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP.join([str(self.sockno),'WRITE_BLOCK',str(size)]))
    565666                self.write(buff)
    566667                return
    567668                       
     
    593694                curhigh,curlow = self.comsock.splitInt(self.pos)
    594695                offhigh,offlow = self.comsock.splitInt(offset)
    595696
    596                 res = self.comsock.backendCommand('QUERY_FILETRANSFER %d%sSEEK%s%d%s%d%s%d%s%d%s%d' % (self.sockno, BACKEND_SEP,BACKEND_SEP,offhigh,BACKEND_SEP,offlow,BACKEND_SEP,whence,BACKEND_SEP,curhigh,BACKEND_SEP,curlow)).split(BACKEND_SEP)
     697                res = self.comsock.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP.join([str(self.sockno),'SEEK',str(offhigh),str(offlow),str(whence),str(curhigh),str(curlow)])).split(BACKEND_SEP)
    597698                self.pos = (int(res[0]) + (int(res[1])<0))*2**32 + int(res[1])
    598699
    599700
    600 class Frontend:
     701class Frontend(object):
    601702        isConnected = False
    602703        socket = None
    603704        host = None
     
    623724                self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    624725                self.socket.settimeout(10)
    625726                self.socket.connect((self.host, self.port))
    626                 if self.recv()[:28] != "MythFrontend Network Control":
     727                if re.search("MythFrontend Network Control.*",self.recv()) is None:
    627728                        self.socket.close()
    628729                        self.socket = None
    629730                        raise Exception('FrontendConnect','Connected socket does not belong to a mythfrontend')
     
    641742                self.socket.send("%s\n" % command)
    642743
    643744        def recv(self,curstr=""):
    644                 def subrecv(self,curstr=""):
     745                curstr = ''
     746                prompt = re.compile('([\r\n.]*)\r\n# ')
     747                while not prompt.search(curstr):
    645748                        try:
    646749                                curstr += self.socket.recv(100)
    647                         except:
    648                                 return None
    649                         if curstr[-4:] != '\r\n# ':
    650                                 curstr = subrecv(self,curstr)
    651                         return curstr
    652                 return subrecv(self)[:-4]
     750                        except socket.error:
     751                                raise MythError('Frontend has closed connection')
     752                        except KeyboardInterrupt:
     753                                raise
     754                return prompt.split(curstr)[0]
    653755
    654756        def sendJump(self,jumppoint):
    655757                """
     
    666768                Returns a tuple containing available jumppoints
    667769                """
    668770                self.send("help jump")
    669                 res = self.recv().split('\r\n')[3:-1]
    670                 points = []
    671                 for point in res:
    672                         spoint = point.split(' - ')
    673                         points.append((spoint[0].rstrip(),spoint[1]))
    674                 return tuple(points)
     771                return tuple(re.findall('(\w+)[ ]+- ([\w /,]+)',self.recv()))
    675772
    676773        def sendKey(self,key):
    677774                """
     
    688785                Returns a tuple containing available special keys
    689786                """
    690787                self.send("help key")
    691                 res = self.recv().split('\r\n')[4]
    692                 keys = []
    693                 for key in res.split(','):
    694                         keys.append(key.strip())
    695                 return tuple(keys)
     788                return tuple(self.recv().split('\r\n')[4].split(', '))
    696789
    697790        def sendQuery(self,query):
    698791                """
     
    706799                Returns a tuple containing available queries
    707800                """
    708801                self.send("help query")
    709                 res = self.recv().split('\r\n')[:-1]
    710                 queries = []
    711                 tmpstr = ""
    712                 for query in res:
    713                         tmpstr += query
    714                         squery = tmpstr.split(' - ')
    715                         if len(squery) == 2:
    716                                 tmpstr = ""
    717                                 queries.append((squery[0].rstrip().lstrip('query '),squery[1]))
    718                 return tuple(queries)
     802                return tuple(re.findall('query ([\w ]*\w+)[ \r\n]+- ([\w /,]+)',self.recv()))
    719803
    720804        def sendPlay(self,play):
    721805                """
     
    732816                Returns a tuple containing available playback commands
    733817                """
    734818                self.send("help play")
    735                 res = self.recv().split('\r\n')[:-1]
    736                 plays = []
    737                 tmpstr = ""
    738                 for play in res:
    739                         tmpstr += play
    740                         splay = tmpstr.split(' - ')
    741                         if len(splay) == 2:
    742                                 tmpstr = ""
    743                                 plays.append((splay[0].rstrip().lstrip('play '),splay[1]))
    744                 return tuple(plays)
    745                        
     819                return tuple(re.findall('play ([\w -:]*\w+)[ \r\n]+- ([\w /:,\(\)]+)',self.recv()))
    746820               
    747821
    748 class Recorder:
     822class Recorder(object):
    749823        """
    750824        Represents a MythTV capture card.
    751825        """
     
    764838                self.videodevice = data[2]
    765839                self.hostname = data[3]
    766840
    767 class Program:
     841class Program(object):
    768842        """
    769843        Represents a program with all the detail known.
    770844        """
     
    885959
    886960                return string
    887961
     962        def setField(self,field,value):
     963                if field not in ['basename','hostname','storagegroup']:
     964                        raise MythError('Invalid field name')
     965                db = MythDB()
     966                c = db.cursor()
     967                c.execute("""UPDATE recorded SET %s = %%s
     968                                WHERE chanid=%%s and starttime=%%s""" % field,
     969                                (value,self.chanid,self.starttime))
     970                c.close()
     971
     972        def setBasename(self,name):
     973                """
     974                Change the file basename pointed to by the recording
     975                """
     976                self.setField('basename',name)
     977
     978        def setHostname(self,name):
     979                """
     980                Change the hostname of the machine which holds the recording
     981                """
     982                self.setField('hostname',name)
     983
     984        def setSG(self,name):
     985                """
     986                Change the storagegroup which holds the recording
     987                """
     988                self.setField('storagegroup',name)
     989
    888990if __name__ == '__main__':
    889991        banner = '\'m\' is a MythTV instance.'
    890992        try:
  • MythTV/MythLog.py

     
    1212INFO = logging.INFO
    1313DEBUG = logging.DEBUG
    1414
    15 class MythLog:
     15class MythLog(object):
    1616        """
    1717        A simple logging class
    1818        """
  • MythTV/MythDB.py

     
    2222        log.Msg(CRITICAL, "MySQLdb (python-mysqldb) is required but is not found.")
    2323        sys.exit(1)
    2424
    25 class MythDB:
     25SCHEMA_VERSION = 1244
     26
     27class MythDB(object):
    2628        """
    2729        A connection to the mythtv database.
    2830        """
     
    4042                # Try to read the config.xml file used by MythTV.
    4143                config_files = [ os.path.expanduser('~/.mythtv/config.xml') ]
    4244                if 'MYTHCONFDIR' in os.environ:
    43                         config_locations.append('%s/config.xml' % os.environ['MYTHCONFDIR'])
     45                        config_files.append('%s/config.xml' % os.environ['MYTHCONFDIR'])
    4446
    4547                found_config = False
    4648                for config_file in config_files:
     
    9698                        raise MythError('Unable to find MythTV configuration file')
    9799
    98100                try:
    99                         self.db = MySQLdb.connect(user=dbconn['user'], host=dbconn['host'], passwd=dbconn['pass'], db=dbconn['name'], use_unicode=True, charset='utf8')
    100                         log.Msg(INFO, 'DB Connection info (host:%s, name:%s, user:%s, pass:%s)', dbconn['host'], dbconn['name'], dbconn['user'], dbconn['pass'])
     101                        self.db = MySQLdb.connect(user=dbconn['user'], host=dbconn['host'],
     102                                        passwd=dbconn['pass'], db=dbconn['name'], use_unicode=True,
     103                                        charset='utf8')
     104                        log.Msg(INFO, 'DB Connection info (host:%s, name:%s, user:%s, pass:%s)',
     105                                        dbconn['host'], dbconn['name'], dbconn['user'], dbconn['pass'])
    101106                except:
    102                         raise MythError('Connection failed for \'%s\'@\'%s\' to database %s using password %s' % (dbconn['user'], dbconn['host'], dbconn['name'], dbconn['pass']))
     107                        raise MythError('Connection failed for \'%s\'@\'%s\' to database %s using password %s' %
     108                                (dbconn['user'], dbconn['host'], dbconn['name'], dbconn['pass']))
    103109
     110                # check schema version
     111                sver = int(self.getSetting('DBSchemaVer'))
     112                if SCHEMA_VERSION != sver:
     113                        log.Msg(CRITICAL, 'DB speaks schema version %d, but we speak version %d',
     114                                        sver, SCHEMA_VERSION)
     115                        raise MythError('Mismatched schema version')
     116
     117        def __del__(self):
     118                self.db.close()
     119                       
     120
    104121        def getAllSettings(self, hostname=None):
    105122                """
    106123                Returns values for all settings.
     
    119136                        c.execute("""
    120137                                        SELECT value, data
    121138                                        FROM settings
    122                                         WHERE hostname LIKE('%s%%')""" %
     139                                        WHERE hostname LIKE(%s)""",
    123140                                        (hostname))
    124141                rows = c.fetchall()
    125142                c.close()
     
    142159                        c.execute("""
    143160                                        SELECT data
    144161                                        FROM settings
    145                                         WHERE value LIKE('%s') AND hostname IS NULL LIMIT 1""" %
     162                                        WHERE value LIKE(%s) AND hostname IS NULL LIMIT 1""",
    146163                                        (value))
    147164                else:
     165                        hostname += '%'
    148166                        c.execute("""
    149167                                        SELECT data
    150168                                        FROM settings
    151                                         WHERE value LIKE('%s') AND hostname LIKE('%s%%') LIMIT 1""" %
     169                                        WHERE value LIKE(%s) AND hostname LIKE(%s) LIMIT 1""",
    152170                                        (value, hostname))
    153171                row = c.fetchone()
    154172                c.close()
     
    166184                c = self.db.cursor()
    167185                ws = None
    168186                ss = None
     187                t = None
    169188
    170189                if hostname is None:
    171                         ws = "WHERE value LIKE ('%s') AND hostname IS NULL" % (value)
    172                         ss = "(value,data) VALUES ('%s','%s')" % (value, data)
     190                        ws = "WHERE value LIKE (%s) AND hostname IS NULL"
     191                        ss = "(data,value) VALUES (%s,%s)"
     192                        t = (data,value)
    173193                else:
    174                         ws = "WHERE value LIKE ('%s') AND hostname LIKE ('%s%%')" % (value, hostname)
    175                         ss = "(value,data,hostname) VALUES ('%s','%s','%s')" % (value, data, hostname)
     194                        hostname += '%'
     195                        ws = "WHERE value LIKE (%s) AND hostname LIKE (%s)"
     196                        ss = "(data,value,hostname) VALUES (%s,%s,%s)"
     197                        t = (data,value,hostname)
    176198
    177                 if c.execute("""UPDATE settings SET data %s LIMIT 1""" % ws) == 0:
    178                         c.execute("""INSERT INTO settings %s""" % ss)
     199                if c.execute("""UPDATE settings SET data=%%s %s LIMIT 1""" % ws, t) == 0:
     200                        c.execute("""INSERT INTO settings %s""" % ss, t)
    179201                c.close()
    180202
    181203        def getCast(self, chanid, starttime, roles=None):
     
    185207                A tuple of strings will return a touple containing all listed roles
    186208                No 'roles' will return a dictionary of tuples
    187209                """
     210                c = self.db.cursor()
     211                if c.execute("""SELECT * FROM recorded WHERE
     212                                chanid=%s AND starttime=%s""",
     213                                (chanid,starttime)) == 0:
     214                        raise MythError('recording does not exist')
    188215                if roles is None:
    189                         c = self.db.cursor()
    190                         length = c.execute("SELECT name,role FROM people,credits WHERE people.person=credits.person AND chanid=%d AND starttime=%d ORDER BY role" % (chanid, starttime))
     216                        length = c.execute("""SELECT name,role
     217                                        FROM people,credits
     218                                        WHERE people.person=credits.person
     219                                        AND chanid=%s AND starttime=%s
     220                                        ORDER BY role""", (chanid, starttime))
    191221                        if length == 0:
    192222                                return ()
    193223                        crole = None
     
    207237                        c.close()
    208238                        return dict
    209239                elif isinstance(roles,str):
    210                         c = self.db.cursor()
    211                         length = c.execute("SELECT name FROM people,credits WHERE people.person=credits.person AND chanid=%d AND starttime=%d AND role='%s'" % (chanid, starttime, roles))
     240                        length = c.execute("""SELECT name
     241                                        FROM people,credits
     242                                        WHERE people.person=credits.person
     243                                        AND chanid=%s AND starttime=%s
     244                                        AND role=%s""", (chanid, starttime, roles))
    212245                        if length == 0:
    213246                                return ()
    214247                        names = []
     
    216249                                names.append(name[0])
    217250                        return tuple(names)
    218251                elif isinstance(roles,tuple):
    219                         c = self.db.cursor()
    220                         length = c.execute("SELECT name FROM people,credits WHERE people.person=credits.person AND chanid=%d AND starttime=%d AND role IN %s" % (chanid, starttime, roles))
     252                        length = c.execute("""SELECT name
     253                                        FROM people,credits
     254                                        WHERE people.person=credits.person
     255                                        AND chanid=%d AND starttime=%d
     256                                        AND role IN %s""" % (chanid, starttime, roles))
    221257                        if length == 0:
    222258                                return ()
    223259                        names = []
    224260                        for name in c.fetchall():
    225261                                names.append(name[0])
    226262                        return tuple(names)
     263                else:
     264                        raise MythError('invalid input format')
    227265
     266        def getStorageGroup(self, group=None, host=None):
     267                """
     268                Returns tuple of dictionaries containing storage group directories
     269                        with the fields 'id', 'group', 'host', and 'dirname'
     270                Takes an optional group and host for filtering
     271                """
     272                c = self.db.cursor()
     273                q1 = 'SELECT * FROM storagegroup'
     274                q2 = 'ORDER BY id'
     275                if host:
     276                        host += '%'
     277                if group and host:
     278                        c.execute("""%s
     279                                WHERE groupname=%%s
     280                                AND hostname like %%s
     281                                %s""" % (q1,q2), (group, host))
     282                elif group:
     283                        c.execute("""%s
     284                                WHERE groupname=%%s
     285                                %s""" % (q1,q2), (group,))
     286                elif host:
     287                        c.execute("""%s
     288                                WHERE hostname like %%s
     289                                %s""" % (q1,q2), (host,))
     290                else:
     291                        c.execute("""%s %s""" % (q1,q2))
     292                ret = []
     293                for i in c.fetchall():
     294                        if not i[3][-1] == '/':
     295                                i[3] += '/'
     296                        ret.append({'id':i[0], 'group':i[1], 'host':i[2], 'dirname':i[3]})
     297                return tuple(ret)
     298
     299        def getChannels(self):
     300                """
     301                Returns a tuple of channel object defined in the database
     302                """
     303                channels = []
     304                c = self.db.cursor()
     305                c.execute("""SELECT * FROM channel""")
     306                for row in c.fetchall():
     307                        channels.append(Channel(row))
     308                c.close()
     309                return tuple(channels)
     310
     311        def getChannel(self,chanid):
     312                """
     313                Returns a single channel object for the given chanid
     314                """
     315                c = self.db.cursor()
     316                if c.execute("""SELECT * FROM channel
     317                                WHERE chanid=%s""", (chanid,)):
     318                        return Channel(c.fetchone())
     319                else:
     320                        return None
     321
     322        def getGuideData(self, chanid, date):
     323                """
     324                Returns tuple of guide data for one channel on one date
     325                """
     326                guide = []
     327                c = self.db.cursor()
     328                c.execute("""SELECT * FROM program
     329                                WHERE chanid=%s AND DATE(starttime)=%s""",
     330                                (chanid,date))
     331                for show in c.fetchall():
     332                        guide.append(Guide(show))
     333                c.close()
     334                return tuple(guide)
     335
    228336        def cursor(self):
    229337                return self.db.cursor()
    230338
    231 class Job:
     339class Job(object):
     340        """
     341        Class for managing tasks within mythtv's jobqueue
     342                Job(jobid)  or  Job(chanid,starttime)
     343                        -- manage existing task
     344                Job(dictionary)
     345                        -- create new task with values in the dictionary
     346        """
     347
     348        NONE         = 0x0000
     349        SYSTEMJOB    = 0x00ff
     350        TRANSCODE    = 0x0001
     351        COMMFLAG     = 0x0002
     352        USERJOB      = 0xff00
     353        USERJOB1     = 0x0100
     354        USERJOB2     = 0x0200
     355        USERJOB3     = 0x0300
     356        USERJOB4     = 0x0400
     357
     358        RUN          = 0x0000
     359        PAUSE        = 0x0001
     360        RESUME       = 0x0002
     361        STOP         = 0x0004
     362        RESTART      = 0x0008
     363
     364        NO_FLAGS     = 0x0000
     365        USE_CUTLIST  = 0x0001
     366        LIVE_REC     = 0x0002
     367        EXTERNAL     = 0x0004
     368
     369        UNKNOWN      = 0x0000
     370        QUEUED       = 0x0001
     371        PENDING      = 0x0002
     372        STARTING     = 0x0003
     373        RUNNING      = 0x0004
     374        STOPPING     = 0x0005
     375        PAUSED       = 0x0006
     376        RETRY        = 0x0007
     377        ERRORING     = 0x0008
     378        ABORTING     = 0x0008
     379        DONE         = 0x0100
     380        FINISHED     = 0x0110
     381        ABORTED      = 0x0120
     382        ERRORED      = 0x0130
     383        CANCELLED    = 0x0140
     384
    232385        jobid = None
    233         chanid = None
    234         starttime = None
    235386        host = None
    236         mythdb = None
     387        db = None
    237388        def __init__(self, *inp):
    238                 if len(inp) == 1:
     389                if isinstance(inp[0],dict):
     390                        if self.insert(inp[0]) is None:
     391                                return none
     392                elif len(inp) == 1:
    239393                        self.jobid = inp[0]
    240394                        self.getProgram()
    241395                elif len(inp) == 2:
     
    243397                        self.starttime = inp[1]
    244398                        self.getJobID()
    245399                else:
    246                         print("improper input length")
     400                        raise MythError('improper input length')
     401                self.getInfo()
     402
     403        def processfields(self,inp):
     404                """
     405                Sanitize lists of fields for SQL queries
     406                """
     407                fields = ['id','chanid','starttime','inserttime','type','cmds',
     408                                'flags','status','statustime','hostname','args',
     409                                'comment','schedruntime']
     410                if isinstance(inp,list):
     411                        for i in range(len(inp)-1,-1,-1):
     412                                if inp[i] not in fields:
     413                                        del inp[i]
     414                elif isinstance(inp,dict):
     415                        for i in inp.keys():
     416                                if i not in fields:
     417                                        del inp[i]
     418                return inp
     419
     420        def get(self,fields,where):
     421                if self.db is None:
     422                        self.db = MythDB()
     423                fields = self.processfields(fields)
     424                where = self.processfields(where)
     425                if (len(fields)==0) or (len(where)==0):
     426                        raise MythError('no valid database fields given')
     427
     428                f = ','.join(fields)
     429                w = 'WHERE '
     430                data = []
     431                if isinstance(where,dict):
     432                        wf = []
     433                        for k,v in where.items():
     434                                wf.append('%s=%%s' % k)
     435                                data.append(v)
     436                        w += ' AND '.join(wf)
     437                else:
     438                        raise MythError("'where' argument must be given as a dictionary")
     439
     440                c = self.db.cursor()
     441                c.execute("""SELECT %s FROM jobqueue %s""" % (f,w), data)
     442
     443                res = c.fetchone()
     444                if len(fields) == 1:
     445                        return res[0]
     446                jdict = {}
     447                for i in range(len(fields)):
     448                        jdict[fields[i]] = res[i]
     449
     450                return jdict
     451
     452        def set(self,data):
     453                if self.db is None:
     454                        self.db = MythDB()
     455                data = self.processfields(data)
     456                if len(data)==0:
     457                        raise MythError('no valid database fields given')
     458
     459                ustr = ', '.join(['%s = %%s' % d for d in data])
     460                values = data.values()
     461                values.append(self.jobid)
     462
     463                c = self.db.cursor()
     464                c.execute("""UPDATE jobqueue SET %s WHERE id=%%s""" % ustr, values)
     465                c.close()
     466
     467        def insert(self,data):
     468                if self.db is None:
     469                        self.db = MythDB()
     470                data = self.processfields(data)
     471                if 'id' in data.keys():
     472                        del data['id']
     473                if len(data)==0:
    247474                        return None
    248                 self.getHost()
    249475
    250         def getProgram(self):
    251                 if self.mythdb is None:
    252                         self.mythdb = MythDB()
    253                 c = self.mythdb.cursor()
    254                 c.execute("SELECT chanid,starttime FROM jobqueue WHERE id=%d" % self.jobid)
    255                 self.chanid, self.starttime = c.fetchone()
     476                # require these three fields for any job creation
     477                for field in ['chanid','starttime','type']:
     478                        if field not in data.keys():
     479                                raise MythError("missing fields in JOB creation")
     480
     481                if 'status' not in data.keys():
     482                        data['status'] = 0x0001          # default to queued
     483                if 'schedruntime' not in data.keys():
     484                        data['schedruntime'] = 'NOW()'   # default to now
     485                data['inserttime'] = 'NOW()'             # override to now
     486
     487                fields = ', '.join(data.keys())
     488                istr = ', '.join(['%s' for d in data])
     489
     490                c = self.db.cursor()
     491                c.execute("""INSERT INTO jobqueue(%s) VALUES(%s)""" % (fields,istr), data.values())
     492                self.jobid = c.lastrowid
    256493                c.close()
     494                return self.jobid
    257495
     496        def getProgram(self):
     497                """
     498                Get chanid and starttime from existing job id
     499                """
     500                res = self.get(['chanid','starttime'],{'id':self.jobid})
     501                self.chanid = res['chanid']
     502                self.starttime = res['starttime']
     503
    258504        def getJobID(self):
    259                 if self.mythdb is None:
    260                         self.mythdb = MythDB()
    261                 if self.jobid is None:
    262                         c = self.mythdb.cursor()
    263                         c.execute("SELECT id FROM jobqueue WHERE chanid=%d AND starttime=%d" % (self.chanid, self.starttime))
    264                         self.jobid = c.fetchone()[0]
    265                         c.close()
     505                """
     506                Get job id from existing chanid and starttime
     507                """
     508                self.jobid = self.get(['id'],{'chanid':self.chanid,'starttime':self.starttime})
    266509                return self.jobid
    267510
    268         def getHost(self):
    269                 if self.mythdb is None:
    270                         self.mythdb = MythDB()
    271                 if self.host is None:
    272                         c = self.mythdb.cursor()
    273                         c.execute("SELECT hostname FROM jobqueue WHERE id=%d" % self.jobid)
    274                         self.host = c.fetchone()[0]
    275                         c.close()
    276                 return self.host
     511        def getInfo(self):
     512                """
     513                Get remaining data fields from existing job id
     514                """
     515                res = self.get(['inserttime','type','cmds','flags','hostname','args'],{'id':self.jobid})
     516                self.inserttime = res['inserttime']
     517                self.type = res['type']
     518                self.cmds = res['cmds']
     519                self.flags = res['flags']
     520                self.host = res['hostname']
     521                self.args = res['args']
    277522
     523        def getComment(self):
     524                """Get comment field from database"""
     525                return self.get(['comment'],{'id':self.jobid})
     526
    278527        def setComment(self,comment):
    279                 if self.mythdb is None:
    280                         self.mythdb = MythDB()
    281                 c = self.mythdb.cursor()
    282                 c.execute("UPDATE jobqueue SET comment='%s' WHERE id=%d" % (comment,self.jobid))
    283                 c.close()
     528                """Set comment field in database"""
     529                self.set({'comment':comment})
    284530
     531        def getStatus(self):
     532                """Get status field from database"""
     533                return self.get(['status'],{'id':self.jobid})
     534
    285535        def setStatus(self,status):
    286                 if self.mythdb is None:
    287                         self.mythdb = MythDB()
    288                 c = self.mythdb.cursor()
    289                 c.execute("UPDATE jobqueue SET status=%d WHERE id=%d" % (status,self.jobid))
    290                 c.close()
     536                """Set status field in database"""
     537                self.set({'status':status})
    291538
     539        def getArgs(self):
     540                """Get argument field from database"""
     541                self.args = self.get(['args'],{'id':self.jobid})
     542                return self.args
     543
     544        def setArgs(self,args):
     545                """Set argument field in database"""
     546                self.set({'args':args})
     547
     548class Channel(object):
     549        """
     550        Represents a single channel from the channel table
     551        """
     552        def __str__(self):
     553                return "%s (%s)" % (self.chanid, self.name)
     554
     555        def __repr__(self):
     556                return "%s (%s)" % (self.chanid, self.name)
     557
     558        def __init__(self,data):
     559                """
     560                Load data into object
     561                """
     562                self.chanid = data[0]
     563                self.channum = data[1]
     564                self.freqid = data[2]
     565                self.sourceid = data[3]
     566                self.callsign = data[4]
     567                self.name = data[5]
     568                self.icon = data[6]
     569                self.finetune = data[7]
     570                self.videofilters = data[8]
     571                self.xmltvid = data[9]
     572                self.recpriority = data[10]
     573                self.contrast = data[11]
     574                self.brightness = data[12]
     575                self.colour = data[13]
     576                self.hue = data[14]
     577                self.tvformat = data[15]
     578                self.visible = data[16]
     579                self.outputfiters = data[17]
     580                self.useonairguide = data[18]
     581                self.mplexid = data[19]
     582                self.serviceid = data[20]
     583                self.tmoffset = data[21]
     584                self.atsc_major_chan = data[22]
     585                self.atsc_minor_chan = data[23]
     586                self.last_record = data[24]
     587                self.default_authority = data[25]
     588                self.commmethod = data[26]
     589
     590class Guide(object):
     591        """
     592        Represents a single program from the program guide
     593        """
     594        def __str__(self):
     595                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     596
     597        def __repr__(self):
     598                return "%s (%s)" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'))
     599
     600        def __init__(self,data):
     601                """
     602                Load data into the object
     603                """
     604                self.chanid = data[0]
     605                self.starttime = data[1]
     606                self.endtime = data[2]
     607                self.title = data[3]
     608                self.subtitle = data[4]
     609                self.description = data[5]
     610                self.category = data[6]
     611                self.category_type = data[7]
     612                self.airdate = data[8]
     613                self.stars = data[9]
     614                self.previouslyshown = data[10]
     615                self.title_pronounce = data[11]
     616                self.stereo = data[12]
     617                self.subtitled = data[13]
     618                self.hdtv = data[14]
     619                self.closecaptioned = data[15]
     620                self.partnumber = data[16]
     621                self.parttotal = data[17]
     622                self.seriesid = data[18]
     623                self.originalairdate = data[19]
     624                self.showtype = data[20]
     625                self.colorcode = data[21]
     626                self.syndicatedepisodenumber = data[22]
     627                self.programid = data[23]
     628                self.manualid = data[24]
     629                self.generic = data[25]
     630                self.listingsource = data[26]
     631                self.first = data[27]
     632                self.last = data[28]
     633                self.audioprop = data[29]
     634                self.subtitletypes = data[30]
     635                self.videoprop = data[31]
     636
    292637if __name__ == '__main__':
    293638        banner = "'mdb' is a MythDB instance."
    294639        try: