mythboxee/patches/MythData.py.orig

1505 lines
56 KiB
Python
Executable File

# -*- 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://<group>@<host>:<port>/<path>
'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<group>.*)@)?(?P<host>[a-zA-Z0-9\.]*)(:[0-9]*)?/(?P<file>.*)')
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://<group>@<host>[:<port>]/<path/to/file>)
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 "<open file 'myth://%s:%s/%s', mode '%s' at %s>" % \
(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 <size> 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 "<FreeSpace '%s@%s' at %s>"\
% (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"<Program '%s','%s' at %s>" % (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"<Uninitialized Record Rule at %s>" % hex(id(self))
return u"<Record Rule '%s', Type %d at %s>" \
% (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"<Uninitialized Recorded at %s>" % hex(id(self))
return u"<Recorded '%s','%s' at %s>" % (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"<Uninitialized RecordedProgram at %s>" % hex(id(self))
return u"<RecordedProgram '%s','%s' at %s>" % (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"<Uninitialized OldRecorded at %s>" % hex(id(self))
return u"<OldRecorded '%s','%s' at %s>" % (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"<Uninitialized Job at %s>" % hex(id(self))
return u"<Job '%s' at %s>" % (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"<Uninitialized Channel at %s>" % hex(id(self))
return u"<Channel '%s','%s' at %s>" % \
(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"<Uninitialized Guide at %s>" % hex(id(self))
return u"<Guide '%s','%s' at %s>" % (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"<Uninitialized Video at %s>" % 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"<Video '%s' at %s>" % (res, hex(id(self)))
def __init__(self, id=None, db=None, raw=None):
DBDataWrite.__init__(self, (id,), db, raw)
if raw is not None:
self._fill_cm(id=self.category)
self.category = self.category_map[1][self.category]
if (id is not None) or (raw is not None):
self.cast = self._Cast((self.intid,), self.db)
self.genre = self._Genre((self.intid,), self.db)
self.country = self._Country((self.intid,), self.db)
def create(self, data=None):
"""Video.create(data=None) -> Video object"""
c = self.db.cursor(self.log)
fields = ' AND '.join(['%s=%%s' % f for f in \
('title','subtitle','season','episode')])
count = c.execute("""SELECT intid FROM videometadata WHERE %s""" %
fields, (self.title, self.subtitle, self.season, self.episode))
if count:
id = c.fetchone()[0]
else:
if data:
if 'category' in data:
self._fill_cm(name=data['category'])
data['category'] = self.category_map[0][data['category']]
self._fill_cm(name=self.category)
self.category = self.category_map[0][self.category]
id = DBDataWrite.create(self, data)
c.close()
self.wheredat = (id,)
self._pull()
self.cast = self._Cast((self.intid,), self.db)
self.genre = self._Genre((self.intid,), self.db)
self.country = self._Country((self.intid,), self.db)
return self
class _Cast( DBDataCRef ):
table = 'videocast'
rtable = 'videometadatacast'
t_ref = 'intid'
t_add = ['cast']
r_ref = 'idcast'
r_add = []
w_field = ['idvideo']
def __repr__(self):
if self.data is None:
self._fill()
if len(self.data) == 0:
return ''
cast = []
for member in self.data:
cast.append(member.cast)
return u', '.join(cast).encode('utf-8')
def add(self, member): DBDataCRef.add(self,(member,))
def delete(self, member): DBDataCRef.delete(self,(member,))
def clean(self):
for member in self:
self.delete(member.cast)
class _Genre( DBDataCRef ):
table = 'videogenre'
rtable = 'videometadatagenre'
t_ref = 'intid'
t_add = ['genre']
r_ref = 'idgenre'
r_add = []
w_field = ['idvideo']
def __repr__(self):
if self.data is None:
self._fill()
if len(self.data) == 0:
return ''
genre = []
for member in self.data:
genre.append(member.genre)
return ', '.join(genre).encode('utf-8')
def add(self, genre): DBDataCRef.add(self,(genre,))
def delete(self, genre): DBDataCRef.delete(self,(genre,))
def clean(self):
for member in self:
self.delete(member.genre)
class _Country( DBDataCRef ):
table = 'videocountry'
rtable = 'videometadatacountry'
t_ref = 'intid'
t_add = ['country']
r_ref = 'idcountry'
r_add = []
w_field = ['idvideo']
def __repr__(self):
if self.data is None:
self._fill()
if len(self.data) == 0:
return ''
country = []
for member in self.data:
country.append(member.country)
return ', '.join(country).encode('utf-8')
def add(self, country): DBDataCRef.add(self,(country,))
def delete(self, country): DBDataCRef.delete(self,(country,))
def clean(self):
for member in self:
self.delete(member.country)
class _Markup( DBDataRef ):
table = 'filemarkup'
wfield = ['filename',]
def _open(self, type, mode='r',nooverwrite=False):
"""
Open file pointer
"""
sgroup = { 'filename':'Videos', 'banner':'Banners',
'coverfile':'Coverart', 'fanart':'Fanart',
'screenshot':'Screenshots', 'trailer':'Trailers'}
if self.data is None:
return None
if type not in sgroup:
raise MythFileError(MythError.FILE_ERROR,
'Invalid type passed to Video._open(): '+str(type))
SG = self.db.getStorageGroup(sgroup[type], self.host)
if len(SG) == 0:
SG = self.db.getStorageGroup('Videos', self.host)
if len(SG) == 0:
raise MythFileError(MythError.FILE_ERROR,
'Could not find MythVideo Storage Groups')
return ftopen('myth://%s@%s/%s' % ( SG[0].groupname,
self.host,
self.data[type]),
mode, False, nooverwrite, self.db)
def delete(self):
"""Video.delete() -> None"""
if self.data is None:
return
self.cast.clean()
self.genre.clean()
self.country.clean()
DBDataWrite.delete(self)
def open(self,mode='r',nooverwrite=False):
"""Video.open(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('filename',mode,nooverwrite)
def openBanner(self,mode='r',nooverwrite=False):
"""Video.openBanner(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('banner',mode,nooverwrite)
def openCoverart(self,mode='r',nooverwrite=False):
"""Video.openCoverart(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('coverfile',mode,nooverwrite)
def openFanart(self,mode='r',nooverwrite=False):
"""Video.openFanart(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('fanart',mode,nooverwrite)
def openScreenshot(self,mode='r',nooverwrite=False):
"""Video.openScreenshot(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('screenshot',mode,nooverwrite)
def openTrailer(self,mode='r',nooverwrite=False):
"""Video.openTrailer(mode='r', nooverwrite=False)
-> file or FileTransfer object"""
return self._open('trailer',mode,nooverwrite)
def getHash(self):
"""Video.getHash() -> file hash"""
if self.host is None:
return None
be = FileOps(self.host)
hash = be.getHash(self.filename, 'Videos')
return hash
def fromFilename(self, filename):
if self.wheredat is not None:
return self
self.filename = filename
filename = filename[:filename.rindex('.')]
for old in ('%20','_','.'):
filename = filename.replace(old, ' ')
sep = '(?:\s?(?:-|/)?\s?)?'
regex1 = re.compile('^(.*[^s0-9])'+sep \
+'(?:s|(?:Season))?'+sep \
+'(\d{1,4})'+sep \
+'(?:[ex/]|Episode)'+sep \
+'(\d{1,3})'+sep \
+'(.*)$', re.I)
regex2 = re.compile('(%s(?:Season%s\d*%s)*%s)$' \
% (sep, sep, sep, sep), re.I)
match1 = regex1.search(filename)
if match1:
self.season = int(match1.group(2))
self.episode = int(match1.group(3))
self.subtitle = match1.group(4)
title = match1.group(1)
match2 = regex2.search(title)
if match2:
title = title[:match2.start()]
self.title = title[title.rindex('/')+1:]
else:
title = filename[filename.rindex('/')+1:]
for left,right in (('(',')'), ('[',']'), ('{','}')):
while left in title:
lin = title.index(left)
rin = title.index(right,lin)
title = title[:lin]+title[rin+1:]
self.title = title
return self
class VideoGrabber( Grabber ):
"""
VideoGrabber(mode, lang='en', db=None) -> VideoGrabber object
'mode' can be of either 'TV' or 'Movie'
"""
logmodule = 'Python MythVideo Grabber'
def __init__(self, mode, lang='en', db=None):
if mode == 'TV':
Grabber.__init__(self, setting='mythvideo.TVGrabber', db=db)
elif mode == 'Movie':
Grabber.__init__(self, setting='mythvideo.MovieGrabber', db=db)
else:
raise MythError('Invalid MythVideo grabber')
self._check_schema('mythvideo.DBSchemaVer',
MVSCHEMA_VERSION, 'MythVideo')
self.append('-l',lang)
self.override = {}
def setOverride(self, data):
# specify overrides for seachTitle function
self.override.update(data)
def searchTitle(self, title, year=None):
"""
VideoGrabber.searchTitle(title, year=None)
-> tuple of tuples of (inetref, title, year)
"""
if title.lower() in self.override:
return ((self.override[title.lower()], title, None),)
regex = re.compile('([0-9]+):(.+?)( \(([0-9]{4})\)\n|\n)')
res = self.command('-M', '"%s"' %title)
ret = []
for m in regex.finditer(res):
m = list(m.group(1,2,4))
m[0] = int(m[0])
if m[2]:
m[2] = int(m[2])
if year and m[2]:
if m[2] != int(year):
continue
ret.append(tuple(m))
return ret
def searchEpisode(self, title, subtitle):
"""
VideoGrabber.searchEpisode(title, subtitle) -> (season, episode)
"""
regex = re.compile("S(?P<season>[0-9]*)E(?P<episode>[0-9]*)")
res = self.command('-N', '"%s"' % title, '"%s"' % subtitle)
match = regex.match(res)
if match:
season = int(match.group('season'))
episode = int(match.group('episode'))
return (season, episode)
else:
return (None, None)
def getData(self, inetref, season=None, episode=None, additional=False):
"""
VideoGrabber.getData(inetref, season=None, episode=None, additional=False)
-> (data, cast, genre, country)
-> (data, cast, genre, country, additional)
'season' and 'episode' are used only for TV grabbers
'data' is a dictionary containing data matching the fields in a
Video object
'cast', 'genre', and 'country' are tuples containing such
'additional' is an optional response with any extra data
"""
if season and episode:
res = self.command('-D', inetref, season, episode)
else:
res = self.command('-D', str(inetref) )
trans = { 'Title':'title', 'Subtitle':'subtitle',
'Year':'year', 'ReleaseDate':'releasedate',
'InetRef':'inetref', 'URL':'homepage',
'Director':'director', 'Plot':'plot',
'UserRating':'stars', 'MovieRating':'rating',
'Runtime':'length', 'Season':'season',
'Episode':'episode', 'Seriesid':'inetref',
'Coverart':'coverfile', 'Fanart':'fanart',
'Banner':'banner', 'Screenshot':'screenshot'}
dat = {}
cast = ()
genre = ()
country = ()
adddict = {}
for point in res.split('\n')[:-1]:
if point.find(':') == -1:
continue
key,val = point.split(':',1)
if key in trans:
dat[trans[key]] = val
elif key == 'Cast':
cast = val.split(', ')
elif key == 'Genres':
genre = val.split(', ')
elif key == 'Countries':
country = val.split(', ')
else:
adddict[key] = val
if 'releasedate' in dat:
dat['releasedate'] = datetime.strptime(dat['releasedate'],\
'%Y-%m-%d').date()
if additional:
return (dat, cast, genre, country, adddict)
return (dat, cast, genre, country)
#### MYTHNETVISION ####
class NetVisionRSSItem( DBData ):
"""
Represents a single program from the netvisionrssitems table
"""
table = 'netvisionrssitems'
where = 'feedtitle=%s AND title=%s'
setwheredat = 'self.feedtitle,self.title'
schema_value = 'NetvisionDBSchemaVer'
schema_local = NVSCHEMA_VERSION
schema_name = 'NetVision'
def __str__(self):
if self.wheredat is None:
return u"<Uninitialized NetVisionRSSItem at %s>" % hex(id(self))
return u"<NetVisionRSSItem '%s@%s' at %s>" % \
(self.title, self.feedtitle, hex(id(self)))
def __repr__(self):
return str(self).encode('utf-8')
class NetVisionTreeItem( DBData ):
"""
Represents a single program from the netvisiontreeitems table
"""
table = 'netvisiontreeitems'
where = 'feedtitle=%s AND path=%s'
setwheredat = 'self.feedtitle,self.path'
schema_value = 'NetvisionDBSchemaVer'
schema_local = NVSCHEMA_VERSION
schema_name = 'NetVision'
def __str__(self):
if self.wheredat is None:
return u"<Uninitialized NetVisionTreeItem at %s>" % hex(id(self))
return u"<NetVisionTreeItem '%s@%s' at %s>" % \
(self.path, self.feedtitle, hex(id(self)))
def __repr__(self):
return str(self).encode('utf-8')
class NetVisionSite( DBData ):
"""
Represents a single site from the netvisionsites table
"""
table = 'netvisionsites'
where = 'name=%'
setwheredat = 'name,'
schema_value = 'NetvisionDBSchemaVer'
schema_local = NVSCHEMA_VERSION
schema_name = 'NetVision'
def __str__(self):
if self.wheredat is None:
return u"<Uninitialized NetVisionSite at %s>" % hex(id(self))
return u"<NetVisionSite '%s','%s' at %s>" % \
(self.name, self.url, hex(id(self)))
def __repr__(self):
return str(self).encode('utf-8')
class NetVisionGrabber( Grabber ):
logmodule = 'Python MythNetVision Grabber'
@staticmethod
def grabberList(types='search,tree', db=None):
db = MythDBBase(db)
db._check_schema('NetvisionDBSchemaVer',
NVSCHEMA_VERSION, 'NetVision')
c = db.cursor(self.log)
log = MythLog('Python MythNetVision Grabber', db=db)
host = gethostname()
glist = []
for t in types.split(','):
c.execute("""SELECT name,commandline
FROM netvision%sgrabbers
WHERE host=%%s""" % t, (host,))
for name,commandline in c.fetchall():
glist.append(NetVisionGrabber(name, t, db, commandline))
c.close()
return glist
def __init__(self, name, type, db=None, commandline=None):
if type not in ('search','tree'):
raise MythError('Invalid NetVisionGrabber() type')
self.type = type
self.name = name
if commandline:
Grabber.__init__(path=commandline, db=db)
else:
db = MythDBBase(db)
self.log = MythLog(self.logmodule, db=self)
if c.execute("""SELECT commandline
FROM netvision%sgrabbers
WHERE name=%%s AND host=%%s""" % type,
(name, gethostname())) == 1:
Grabber.__init__(path=c.fetchone()[0], db=db)
c.close()
else:
c.close()
raise MythError('NetVisionGrabber not found in database')
def searchXML(self, title, page=1):
return etree.fromstring(\
self.command('-p',page,'-S','"%s"' %title)).getroot()
def treeXML(self):
return etree.fromstring(self.command('-T'))
def setUpdated(self):
if self.type is not 'tree':
raise MythError('Can only update tree-type grabbers')
c = db.cursor(self.log)
c.execute("""UPDATE netvision%sgrabbers SET update=NOW()
WHERE name=%%s AND host=%%s""" % type,
(self.name, gethostname()))
c.close()