# -*- coding: utf-8 -*- """ Provides data access classes for accessing and managing MythTV data """ from MythStatic import * from MythBase import * import re, sys, socket, os import xml.etree.cElementTree as etree from time import mktime, strftime, strptime from datetime import date, time, datetime from socket import gethostbyaddr, gethostname #### FILE ACCESS #### def ftopen(file, type, forceremote=False, nooverwrite=False, db=None): """ ftopen(file, type, forceremote=False, nooverwrite=False, db=None) -> FileTransfer object -> file object Method will attempt to open file locally, falling back to remote access over mythprotocol if necessary. 'forceremote' will force a FileTransfer object if possible. 'file' takes a standard MythURI: myth://@:/ 'type' takes a 'r' or 'w' 'nooverwrite' will refuse to open a file writable, if a local file is found. """ db = MythDBBase(db) log = MythLog('Python File Transfer', db=db) reuri = re.compile(\ 'myth://((?P.*)@)?(?P[a-zA-Z0-9\.]*)(:[0-9]*)?/(?P.*)') reip = re.compile('(?:\d{1,3}\.){3}\d{1,3}') if type not in ('r','w'): raise TypeError("File I/O must be of type 'r' or 'w'") # process URI (myth://@[:]/) match = reuri.match(file) if match is None: raise MythError('Invalid FileTransfer input string: '+file) host = match.group('host') filename = match.group('file') sgroup = match.group('group') if sgroup is None: sgroup = 'Default' # get full system name if reip.match(host): c = db.cursor(log) if c.execute("""SELECT hostname FROM settings WHERE value='BackendServerIP' AND data=%s""", host) == 0: c.close() raise MythDBError(MythError.DB_SETTING, 'BackendServerIP', backend) host = c.fetchone()[0] c.close() # user forced to remote access if forceremote: if (type == 'w') and (filename.find('/') != -1): raise MythFileError(MythError.FILE_FAILED_WRITE, file, 'attempting remote write outside base path') return FileTransfer(host, filename, sgroup, type, db) sgs = db.getStorageGroup(groupname=sgroup) if type == 'w': # prefer local storage always for i in range(len(sgs)-1,-1,-1): if not sgs[i].local: sgs.pop(i) else: st = os.statvfs(sgs[i].dirname) sgs[i].free = st[0]*st[3] if len(sgs) > 1: # choose path with most free space sg = sgs.pop() while len(sgs): if sgs[0].free > sg.free: sg = sgs.pop() else: sgs.pop() # check that folder exists if filename.find('/') != -1: path = sg.dirname+filename.rsplit('/',1)[0] if not os.access(path, os.F_OK): os.makedirs(path) if nooverwrite: if os.access(sg.dirname+filename, os.F_OK): raise MythDBError(MythError.FILE_FAILED_WRITE, file, 'refusing to overwrite existing file') log(log.FILE, 'Opening local file (w)', sg.dirname+filename) return open(sg.dirname+filename, 'w') elif len(sgs) == 1: if filename.find('/') != -1: path = sgs[0].dirname+filename.rsplit('/',1)[0] if not os.access(path, os.F_OK): os.makedirs(path) if nooverwrite: if os.access(sgs[0].dirname+filename, os.F_OK): raise MythFileError(MythError.FILE_FAILED_WRITE, file, 'refusing to overwrite existing file') log(log.FILE, 'Opening local file (w)', sgs[0].dirname+filename) return open(sgs[0].dirname+filename, 'w') else: if filename.find('/') != -1: raise MythFileError(MythError.FILE_FAILED_WRITE, file, 'attempting remote write outside base path') return FileTransfer(host, filename, sgroup, 'w', db) else: # search for file in local directories for sg in sgs: if sg.local: if os.access(sg.dirname+filename, os.F_OK): # file found, open local log(log.FILE, 'Opening local file (r)', sg.dirname+filename) return open(sg.dirname+filename, type) # file not found, open remote return FileTransfer(host, filename, sgroup, type, db) class FileTransfer( MythBEConn ): """ A connection to mythbackend intended for file transfers. Emulates the primary functionality of the local 'file' object. """ logmodule = 'Python FileTransfer' def __repr__(self): return "" % \ (self.sgroup, self.host, self.filename, \ self.type, hex(id(self))) def __init__(self, host, filename, sgroup, type, db=None): if type not in ('r','w'): raise MythError("FileTransfer type must be read ('r') "+ "or write ('w')") self.host = host self.filename = filename self.sgroup = sgroup self.type = type self.tsize = 2**15 self.tmax = 2**17 self.count = 0 self.step = 2**12 self.open = False # create control socket self.control = MythBEBase(host, 'Playback', db=db) self.control.log.module = 'Python FileTransfer Control' # continue normal Backend initialization MythBEConn.__init__(self, host, type, db=db) self.open = True def announce(self, type): # replacement announce for Backend object if type == 'w': self.w = True elif type == 'r': self.w = False res = self.backendCommand('ANN FileTransfer %s %d %d %s' \ % (socket.gethostname(), self.w, False, BACKEND_SEP.join(['-1',self.filename,self.sgroup]))) if res.split(BACKEND_SEP)[0] != 'OK': raise MythError(MythError.PROTO_ANNOUNCE, self.host, self.port, res) else: sp = res.split(BACKEND_SEP) self.sockno = int(sp[1]) self.pos = 0 self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3]) def __del__(self): if not self.open: return self.control.backendCommand('QUERY_FILETRANSFER '\ +BACKEND_SEP.join([str(self.sockno), 'DONE'])) self.socket.shutdown(1) self.socket.close() self.open = False def tell(self): """FileTransfer.tell() -> current offset in file""" return self.pos def close(self): """FileTransfer.close() -> None""" self.__del__() def rewind(self): """FileTransfer.rewind() -> None""" self.seek(0) def read(self, size): """ FileTransfer.read(size) -> string of characters Requests over 128KB will be buffered internally. """ def recv(self, size): buff = '' while len(buff) < size: buff += self.socket.recv(size-len(buff)) return buff if self.w: raise MythFileError('attempting to read from a write-only socket') if size == 0: return '' final = self.pos+size if final > self.size: final = self.size buff = '' while self.pos < final: self.count += 1 size = final - self.pos if size > self.tsize: size = self.tsize res = self.control.backendCommand('QUERY_FILETRANSFER '+\ BACKEND_SEP.join([ str(self.sockno), 'REQUEST_BLOCK', str(size)])) #print '%s - %s' % (int(res), size) if int(res) == size: if (self.count == 10) and (self.tsize < self.tmax) : self.count = 0 self.tsize += self.step else: if int(res) == -1: self.seek(self.pos) continue size = int(res) self.count = 0 self.tsize -= self.step if self.tsize < self.step: self.tsize = self.step buff += recv(self, size) self.pos += size return buff def write(self, data): """ FileTransfer.write(data) -> None Requests over 128KB will be buffered internally """ if not self.w: raise MythFileError('attempting to write to a read-only socket') while len(data) > 0: size = len(data) if size > self.tsize: buff = data[self.tsize:] data = data[:self.tsize] else: buff = data data = '' self.pos += int(self.socket.send(data)) self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\ .join([str(self.sockno),'WRITE_BLOCK',str(size)])) return def seek(self, offset, whence=0): """ FileTransfer.seek(offset, whence=0) -> None Seek 'offset' number of bytes whence == 0 - from start of file 1 - from current position 2 - from end of file """ if whence == 0: if offset < 0: offset = 0 elif offset > self.size: offset = self.size elif whence == 1: if offset + self.pos < 0: offset = -self.pos elif offset + self.pos > self.size: offset = self.size - self.pos elif whence == 2: if offset > 0: offset = 0 elif offset < -self.size: offset = -self.size whence = 0 offset = self.size+offset curhigh,curlow = self.control.splitInt(self.pos) offhigh,offlow = self.control.splitInt(offset) res = self.control.backendCommand('QUERY_FILETRANSFER '+BACKEND_SEP\ .join([str(self.sockno),'SEEK',str(offhigh), str(offlow),str(whence),str(curhigh), str(curlow)])\ ).split(BACKEND_SEP) self.pos = self.control.joinInt(int(res[0]),int(res[1])) class FileOps( MythBEBase ): __doc__ = MythBEBase.__doc__+""" getRecording() - return a Program object for a recording deleteRecording() - notify the backend to delete a recording forgetRecording() - allow a recording to re-record deleteFile() - notify the backend to delete a file in a storage group getHash() - return the hash of a file in a storage group reschedule() - trigger a run of the scheduler """ logmodule = 'Python Backend FileOps' def getRecording(self, chanid, starttime): """FileOps.getRecording(chanid, starttime) -> Program object""" res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' \ % (chanid, starttime)).split(BACKEND_SEP) if res[0] == 'ERROR': return None else: return Program(res[1:], db=self.db) def deleteRecording(self, program, force=False): """ FileOps.deleteRecording(program, force=False) -> retcode 'force' will force a delete even if the file cannot be found retcode will be -1 on success, -2 on failure """ command = 'DELETE_RECORDING' if force: command = 'FORCE_DELETE_RECORDING' return self.backendCommand(BACKEND_SEP.join(\ [command,program.toString()])) def forgetRecording(self, program): """FileOps.forgetRecording(program) -> None""" self.backendCommand(BACKEND_SEP.join(['FORGET_RECORDING', program.toString()])) def deleteFile(self, file, sgroup): """FileOps.deleteFile(file, storagegroup) -> retcode""" return self.backendCommand(BACKEND_SEP.join(\ ['DELETE_FILE',file,sgroup])) def getHash(self, file, sgroup): """FileOps.getHash(file, storagegroup) -> hash string""" return self.backendCommand(BACKEND_SEP.join((\ 'QUERY_FILE_HASH',file, sgroup))) def reschedule(self, recordid=-1): """FileOps.reschedule() -> None""" self.backendCommand('RESCHEDULE_RECORDINGS '+str(recordid)) class FreeSpace( DictData ): """Represents a FreeSpace entry.""" field_order = [ 'host', 'path', 'islocal', 'disknumber', 'sgroupid', 'blocksize', 'ts_high', 'ts_low', 'us_high', 'us_low'] field_type = [3, 3, 2, 0, 0, 0, 0, 0, 0, 0] def __str__(self): return ""\ % (self.path, self.host, hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, raw): DictData.__init__(self, raw) self.totalspace = self.joinInt(self.ts_high, self.ts_low) self.usedspace = self.joinInt(self.us_high, self.us_low) self.freespace = self.totalspace - self.usedspace #### RECORDING ACCESS #### class Program( DictData ): """Represents a program with all detail returned by the backend.""" # recstatus TUNERBUSY = -8 LOWDISKSPACE = -7 CANCELLED = -6 DELETED = -5 ABORTED = -4 RECORDED = -3 RECORDING = -2 WILLRECORD = -1 UNKNOWN = 0 DONTRECORD = 1 PREVIOUSRECORDING = 2 CURRENTRECORDING = 3 EARLIERSHOWING = 4 TOOMANYRECORDINGS = 5 NOTLISTED = 6 CONFLICT = 7 LATERSHOWING = 8 REPEAT = 9 INACTIVE = 10 NEVERRECORD = 11 field_order = [ 'title', 'subtitle', 'description', 'category', 'chanid', 'channum', 'callsign', 'channame', 'filename', 'fs_high', 'fs_low', 'starttime', 'endtime', 'duplicate', 'shareable', 'findid', 'hostname', 'sourceid', 'cardid', 'inputid', 'recpriority', 'recstatus', 'recordid', 'rectype', 'dupin', 'dupmethod', 'recstartts', 'recendts', 'repeat', 'programflags', 'recgroup', 'commfree', 'outputfilters', 'seriesid', 'programid', 'lastmodified', 'stars', 'airdate', 'hasairdate', 'playgroup', 'recpriority2', 'parentid', 'storagegroup', 'audio_props', 'video_props', 'subtitle_type','year'] field_type = [ 3, 3, 3, 3, 0, 3, 3, 3, 3, 0, 0, 4, 4, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 3, 0, 0, 4, 4, 0, 3, 3, 0, 3, 3, 3, 3, 1, 3, 0, 3, 0, 3, 3, 0, 0, 0, 0] def __str__(self): return u"" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, raw=None, db=None, etree=None): if raw: DictData.__init__(self, raw) elif etree: xmldat = etree.attrib xmldat.update(etree.find('Channel').attrib) if etree.find('Recording'): xmldat.update(etree.find('Recording').attrib) dat = {} if etree.text: dat['description'] = etree.text.strip() for key in ('title','subTitle','seriesId','programId','airdate', 'category','hostname','chanNum','callSign','playGroup', 'recGroup','rectype','programFlags','chanId','recStatus', 'commFree','stars'): if key in xmldat: dat[key.lower()] = xmldat[key] for key in ('startTime','endTime','lastModified', 'recStartTs','recEndTs'): if key in xmldat: dat[key.lower()] = str(int(mktime(strptime( xmldat[key], '%Y-%m-%dT%H:%M:%S')))) if 'fileSize' in xmldat: dat['fs_high'],dat['fs_low'] = \ self.splitInt(int(xmldat['fileSize'])) raw = [] defs = (0,0,0,'',0) for i in range(len(self.field_order)): if self.field_order[i] in dat: raw.append(dat[self.field_order[i]]) else: raw.append(defs[self.field_type[i]]) DictData.__init__(self, raw) else: raise InputError("Either 'raw' or 'etree' must be provided") self.db = MythDBBase(db) self.filesize = self.joinInt(self.fs_high,self.fs_low) def toString(self): """ Program.toString() -> string representation for use with backend protocol commands """ data = [] for i in range(0,PROGRAM_FIELDS): if self.data[self.field_order[i]] == None: datum = '' elif self.field_type[i] == 0: datum = str(self.data[self.field_order[i]]) elif self.field_type[i] == 1: datum = locale.format("%0.6f", self.data[self.field_order[i]]) elif self.field_type[i] == 2: datum = str(int(self.data[self.field_order[i]])) elif self.field_type[i] == 3: datum = self.data[self.field_order[i]] elif self.field_type[i] == 4: datum = str(int(mktime(self.data[self.field_order[i]].\ timetuple()))) data.append(datum) return BACKEND_SEP.join(data) def delete(self, force=False, rerecord=False): """ Program.delete(force=False, rerecord=False) -> retcode Informs backend to delete recording and all relevent data. 'force' forces a delete if the file cannot be found. 'rerecord' sets the file as recordable in oldrecorded """ be = FileOps(db=self.db) res = int(be.deleteRecording(self, force=force)) if res < -1: raise MythBEError('Failed to delete file') if rerecord: be.forgetRecording(self) return res def getRecorded(self): """Program.getRecorded() -> Recorded object""" return Recorded((self.chanid,self.recstartts), db=self.db) def open(self, type='r'): """Program.open(type='r') -> file or FileTransfer object""" if type != 'r': raise MythFileError(MythError.FILE_FAILED_WRITE, self.filename, 'Program () objects cannot be opened for writing') if not self.filename.startswith('myth://'): self.filename = 'myth://%s/%s' % (self.hostname, self.filename) return ftopen(self.filename, 'r') class Record( DBDataWrite ): """ Record(id=None, db=None, raw=None) -> Record object """ kNotRecording = 0 kSingleRecord = 1 kTimeslotRecord = 2 kChannelRecord = 3 kAllRecord = 4 kWeekslotRecord = 5 kFindOneRecord = 6 kOverrideRecord = 7 kDontRecord = 8 kFindDailyRecord = 9 kFindWeeklyRecord = 10 table = 'record' where = 'recordid=%s' setwheredat = 'self.recordid,' defaults = {'recordid':None, 'type':kAllRecord, 'title':u'Unknown', 'subtitle':'', 'description':'', 'category':'', 'station':'', 'seriesid':'', 'search':0, 'last_record':datetime(1900,1,1), 'next_record':datetime(1900,1,1), 'last_delete':datetime(1900,1,1)} logmodule = 'Python Record' def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" \ % (self.title, self.type, hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, id=None, db=None, raw=None): DBDataWrite.__init__(self, (id,), db, raw) def create(self, data=None): """Record.create(data=None) -> Record object""" self.wheredat = (DBDataWrite.create(self, data),) self._pull() FileOps(db=self.db).reschedule(self.recordid) return self def update(self, *args, **keywords): DBDataWrite.update(*args, **keywords) FileOps(db=self.db).reschedule(self.recordid) class Recorded( DBDataWrite ): """ Recorded(data=None, db=None, raw=None) -> Recorded object 'data' is a tuple containing (chanid, storagegroup) """ table = 'recorded' where = 'chanid=%s AND starttime=%s' setwheredat = 'self.chanid,self.starttime' defaults = {'title':u'Unknown', 'subtitle':'', 'description':'', 'category':'', 'hostname':'', 'bookmark':0, 'editing':0, 'cutlist':0, 'autoexpire':0, 'commflagged':0, 'recgroup':'Default', 'seriesid':'', 'programid':'', 'lastmodified':'CURRENT_TIMESTAMP', 'filesize':0, 'stars':0, 'previouslyshown':0, 'preserve':0, 'bookmarkupdate':0, 'findid':0, 'deletepending':0, 'transcoder':0, 'timestretch':1, 'recpriority':0, 'playgroup':'Default', 'profile':'No', 'duplicate':1, 'transcoded':0, 'watched':0, 'storagegroup':'Default'} logmodule = 'Python Recorded' class _Cast( DBDataCRef ): table = 'people' rtable = 'recordedcredits' t_ref = 'person' t_add = ['name'] r_ref = 'person' r_add = ['role'] w_field = ['chanid','starttime'] def __repr__(self): if self.data is None: self._fill() if len(self.data) == 0: return 'No cast' cast = [] for member in self.data: cast.append(member.name) return ', '.join(cast).encode('utf-8') class _Seek( DBDataRef ): table = 'recordedseek' wfield = ['chanid','starttime'] class _Markup( DBDataRef ): table = 'recordedmarkup' wfield = ['chanid','starttime'] def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, data=None, db=None, raw=None): DBDataWrite.__init__(self, data, db, raw) if (data is not None) or (raw is not None): self.cast = self._Cast(self.wheredat, self.db) self.seek = self._Seek(self.wheredat, self.db) self.markup = self._Markup(self.wheredat, self.db) def create(self, data=None): """Recorded.create(data=None) -> Recorded object""" DBDataWrite.create(self, data) self.wheredat = (self.chanid,self.starttime) self._pull() self.cast = self._Cast(self.wheredat, self.db) self.seek = self._Seek(self.wheredat, self.db) self.markup = self._Markup(self.wheredat, self.db) return self def delete(self, force=False, rerecord=False): """ Recorded.delete(force=False, rerecord=False) -> retcode Informs backend to delete recording and all relevent data. 'force' forces a delete if the file cannot be found. 'rerecord' sets the file as recordable in oldrecorded """ return self.getProgram().delete(force, rerecord) def open(self, type='r'): """Recorded.open(type='r') -> file or FileTransfer object""" return ftopen("myth://%s@%s/%s" % ( self.storagegroup, \ self.hostname,\ self.basename), type, db=self.db) def getProgram(self): """Recorded.getProgram() -> Program object""" be = FileOps(db=self.db) return be.getRecording(self.chanid, int(self.starttime.strftime('%Y%m%d%H%M%S'))) def getRecordedProgram(self): """Recorded.getRecordedProgram() -> RecordedProgram object""" return RecordedProgram((self.chanid,self.progstart), db=self.db) def formatPath(self, path, replace=None): """ Recorded.formatPath(path, replace=None) -> formatted path string 'path' string is formatted as per mythrename.pl """ for (tag, data) in (('T','title'), ('S','subtitle'), ('R','description'), ('C','category'), ('U','recgroup'), ('hn','hostname'), ('c','chanid') ): tmp = unicode(self[data]).replace('/','-') path = path.replace('%'+tag, tmp) for (data, pre) in ( ('starttime','%'), ('endtime','%e'), ('progstart','%p'),('progend','%pe') ): for (tag, format) in (('y','%y'),('Y','%Y'),('n','%m'),('m','%m'), ('j','%d'),('d','%d'),('g','%I'),('G','%H'), ('h','%I'),('H','%H'),('i','%M'),('s','%S'), ('a','%p'),('A','%p') ): path = path.replace(pre+tag, self[data].strftime(format)) for (tag, format) in (('y','%y'),('Y','%Y'),('n','%m'),('m','%m'), ('j','%d'),('d','%d')): path = path.replace('%o'+tag, self['originalairdate'].strftime(format)) path = path.replace('%-','-') path = path.replace('%%','%') path += '.'+self['basename'].split('.')[-1] # clean up for windows if replace is not None: for char in ('\\',':','*','?','"','<','>','|'): path = path.replace(char, replace) return path class RecordedProgram( DBDataWrite ): """ RecordedProgram(data=None, db=None, raw=None) -> RecordedProgram object 'data' is a tuple containing (chanid, storagegroup) """ table = 'recordedprogram' where = 'chanid=%s AND starttime=%s' setwheredat = 'self.chanid,self.starttime' defaults = {'title':'', 'subtitle':'', 'category':'', 'category_type':'', 'airdate':0, 'stars':0, 'previouslyshown':0, 'title_pronounce':'', 'stereo':0, 'subtitled':0, 'hdtv':0, 'partnumber':0, 'closecaptioned':0, 'parttotal':0, 'seriesid':'', 'originalairdate':'', 'showtype':u'', 'colorcode':'', 'syndicatedepisodenumber':'', 'programid':'', 'manualid':0, 'generic':0, 'first':0, 'listingsource':0, 'last':0, 'audioprop':u'','videoprop':u'', 'subtitletypes':u''} logmodule = 'Python RecordedProgram' def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def create(self, data=None): """RecordedProgram.create(data=None) -> RecordedProgram object""" DBDataWrite.create(self, data) self.wheredat = (self.chanid, self.starttime) self._pull() return self class OldRecorded( DBDataWrite ): """ OldRecorded(data=None, db=None, raw=None) -> OldRecorded object 'data' is a tuple containing (chanid, storagegroup) """ table = 'oldrecorded' where = 'chanid=%s AND starttime=%s' setwheredat = 'self.chanid,self.starttime' defaults = {'title':'', 'subtitle':'', 'category':'', 'seriesid':'', 'programid':'', 'findid':0, 'recordid':0, 'station':'', 'rectype':0, 'duplicate':0, 'recstatus':-3, 'reactivate':0, 'generic':0} logmodule = 'Python OldRecorded' def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def create(self, data=None): """OldRecorded.create(data=None) -> OldRecorded object""" DBDataWrite.create(self, data) self.wheredat = (self.chanid, self.starttime) self._pull() return self def setDuplicate(self, record=False): """ OldRecorded.setDuplicate(record=False) -> None Toggles re-recordability """ c = self.db.cursor(self.log) c.execute("""UPDATE oldrecorded SET duplicate=%%s WHERE %s""" % self.where, \ tuple([record]+list(self.wheredat))) FileOps(db=self.db).reschedule(0) def update(self, *args, **keywords): """OldRecorded entries can not be altered""" return def delete(self): """OldRecorded entries cannot be deleted""" return class Job( DBDataWrite ): """ Job(id=None, chanid=None, starttime=None, db=None, raw=None) -> Job object Can be initialized with a Job id, or chanid and starttime. """ # types NONE = 0x0000 SYSTEMJOB = 0x00ff TRANSCODE = 0x0001 COMMFLAG = 0x0002 USERJOB = 0xff00 USERJOB1 = 0x0100 USERJOB2 = 0x0200 USERJOB3 = 0x0300 USERJOB4 = 0x0400 # cmds RUN = 0x0000 PAUSE = 0x0001 RESUME = 0x0002 STOP = 0x0004 RESTART = 0x0008 # flags NO_FLAGS = 0x0000 USE_CUTLIST = 0x0001 LIVE_REC = 0x0002 EXTERNAL = 0x0004 # statuses UNKNOWN = 0x0000 QUEUED = 0x0001 PENDING = 0x0002 STARTING = 0x0003 RUNNING = 0x0004 STOPPING = 0x0005 PAUSED = 0x0006 RETRY = 0x0007 ERRORING = 0x0008 ABORTING = 0x0008 DONE = 0x0100 FINISHED = 0x0110 ABORTED = 0x0120 ERRORED = 0x0130 CANCELLED = 0x0140 table = 'jobqueue' logmodule = 'Python Jobqueue' defaults = {'id': None} def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % (self.id, hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, id=None, chanid=None, starttime=None, \ db=None, raw=None): self.__dict__['where'] = 'id=%s' self.__dict__['setwheredat'] = 'self.id,' if raw is not None: DBDataWrite.__init__(self, None, db, raw) elif id is not None: DBDataWrite.__init__(self, (id,), db, None) elif (chanid is not None) and (starttime is not None): self.__dict__['where'] = 'chanid=%s AND starttime=%s' DBDataWrite.__init__(self, (chanid,starttime), db, None) else: DBDataWrite.__init__(self, None, db, None) def create(self, data=None): """Job.create(data=None) -> Job object""" id = DBDataWrite.create(self, data) self.where = 'id=%s' self.wheredat = (id,) return self def setComment(self,comment): """Job.setComment(comment) -> None, updates comment""" self.comment = comment self.update() def setStatus(self,status): """Job.setStatus(Status) -> None, updates status""" self.status = status self.update() class Channel( DBDataWrite ): """Channel(chanid=None, data=None, raw=None) -> Channel object""" table = 'channel' where = 'chanid=%s' setwheredat = 'self.chanid,' defaults = {'icon':'none', 'videofilters':'', 'callsign':u'', 'xmltvid':'', 'recpriority':0, 'contrast':32768, 'brightness':32768, 'colour':32768, 'hue':32768, 'tvformat':u'Default', 'visible':1, 'outputfilters':'', 'useonairguide':0, 'atsc_major_chan':0, 'tmoffset':0, 'default_authority':'', 'commmethod':-1, 'atsc_minor_chan':0, 'last_record':datetime(1900,1,1)} logmodule = 'Python Channel' def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % \ (self.chanid, self.name, hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, chanid=None, db=None, raw=None): DBDataWrite.__init__(self, (chanid,), db, raw) def create(self, data=None): """Channel.create(data=None) -> Channel object""" DBDataWrite.create(self, data) self.wheredat = (self.chanid,) self._pull() return self class Guide( DBData ): """ Guide(data=None, db=None, raw=None) -> Guide object Data is a tuple of (chanid, starttime). """ table = 'program' where = 'chanid=%s AND starttime=%s' setwheredat = 'self.chanid,self.starttime' logmodule = 'Python Guide' def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) return u"" % (self.title, self.starttime.strftime('%Y-%m-%d %H:%M:%S'), hex(id(self))) def __repr__(self): return str(self).encode('utf-8') def __init__(self, data=None, db=None, raw=None, etree=None): if etree: db = MythDBBase(db) dat = {'chanid':etree[0]} attrib = etree[1].attrib for key in ('title','subTitle','category','seriesId', 'hostname','programId','airdate'): if key in attrib: dat[key.lower()] = attrib[key] if 'stars' in attrib: dat['stars'] = locale.atof(attrib['stars']) if etree[1].text: dat['description'] = etree[1].text.strip() for key in ('startTime','endTime','lastModified'): if key in attrib: dat[key.lower()] = datetime.strptime( attrib[key],'%Y-%m-%dT%H:%M:%S') raw = [] for key in db.tablefields.program: if key in dat: raw.append(dat[key]) else: raw.append(None) DBData.__init__(self, db=db, raw=raw) else: DBData.__init__(self, data=data, db=db, raw=raw) def record(self, type=Record.kAllRecord): rec = Record(db=self.db) for key in ('chanid','title','subtitle','description', 'category', 'seriesid','programid'): rec[key] = self[key] rec.startdate = self.starttime.date() rec.starttime = self.starttime-datetime.combine(rec.startdate, time()) rec.enddate = self.endtime.date() rec.endtime = self.endtime-datetime.combine(rec.enddate, time()) rec.station = Channel(self.chanid, db=self.db).callsign rec.type = type return rec.create() #### MYTHVIDEO #### class Video( DBDataWrite ): """Video(id=None, db=None, raw=None) -> Video object""" table = 'videometadata' where = 'intid=%s' setwheredat = 'self.intid,' defaults = {'subtitle':u'', 'director':u'Unknown', 'rating':u'NR', 'inetref':u'00000000', 'year':1895, 'userrating':0.0, 'length':0, 'showlevel':1, 'coverfile':u'No Cover', 'host':u'', 'intid':None, 'homepage':u'', 'watched':False, 'category':'none', 'browse':True, 'hash':u'', 'season':0, 'episode':0, 'releasedate':date(1,1,1), 'childid':-1, 'insertdate': datetime.now()} logmodule = 'Python Video' schema_value = 'mythvideo.DBSchemaVer' schema_local = MVSCHEMA_VERSION schema_name = 'MythVideo' category_map = [{'None':0},{0:'None'}] def _fill_cm(self, name=None, id=None): if name: if name not in self.category_map[0]: c = self.db.cursor(self.log) q1 = """SELECT intid FROM videocategory WHERE category=%s""" q2 = """INSERT INTO videocategory SET category=%s""" if c.execute(q1, name) == 0: c.execute(q2, name) c.execute(q1, name) id = c.fetchone()[0] self.category_map[0][name] = id self.category_map[1][id] = name elif id: if id not in self.category_map[1]: c = self.db.cursor(self.log) if c.execute("""SELECT category FROM videocategory WHERE intid=%s""", id) == 0: raise MythDBError('Invalid ID found in videometadata.category') else: name = c.fetchone()[0] self.category_map[0][name] = id self.category_map[1][id] = name def _pull(self): DBDataWrite._pull(self) self._fill_cm(id=self.category) self.category = self.category_map[1][self.category] def _push(self): name = self.category self._fill_cm(name=name) self.category = self.category_map[0][name] DBDataWrite._push(self) self.category = name def __repr__(self): return str(self).encode('utf-8') def __str__(self): if self.wheredat is None: return u"" % hex(id(self)) res = self.title if self.season and self.episode: res += u' - %dx%02d' % (self.season, self.episode) if self.subtitle: res += u' - '+self.subtitle return u"