mythboxee/mythtv/MythFunc.py

1171 lines
41 KiB
Python
Executable File

# -*- coding: utf-8 -*-
"""
Provides base classes for accessing MythTV
"""
from MythStatic import *
from MythBase import *
from MythData import *
import os, re, socket
from datetime import datetime
class databaseSearch( object ):
# decorator class for database searches
def __init__(self, func):
# set function and update strings
self.func = func
self.__doc__ = self.func.__doc__
self.__name__ = self.func.__name__
self.__module__ = self.func.__module__
# process joins
res = list(self.func(self, True))
self.table = res[0]
self.dbclass = res[1]
self.require = res[2]
if len(res) > 3:
self.joins = res[3:]
else:
self.joins = ()
def __get__(self, inst, own):
# set instance and return self
self.inst = inst
return self
def __call__(self, **kwargs):
where = []
fields = []
joinbit = 0
# loop through inputs
for key, val in kwargs.items():
if val is None:
continue
# process custom query
if key == 'custom':
custwhere = {}
custwhere.update(val)
for k,v in custwhere.items():
where.append(k)
fields.append(v)
continue
# let function process remaining queries
res = self.func(self.inst, key=key, value=val)
if res is None:
raise TypeError("%s got an unexpected keyword arguemnt '%s'" \
% (self.__name__, key))
where.append(res[0])
fields.append(res[1])
joinbit = joinbit|res[2]
for key in self.require:
if key not in kwargs:
res = self.func(self.inst, key=key)
if res is None:
continue
where.append(res[0])
fields.append(res[1])
joinbit = joinbit|res[2]
# build joins
joins = []
for i in range(len(self.joins)):
if (2**i)&joinbit:
if len(self.joins[i]) == 3:
on = ' AND '.join(['%s.%s=%s.%s' % \
(self.joins[i][0], field, self.joins[i][1], field)\
for field in self.joins[i][2]])
else:
on = ' AND '.join(['%s.%s=%s.%s' % \
(self.joins[i][0], ffield, self.joins[i][1], tfield)\
for ffield, tfield in \
zip(self.joins[i][2],self.joins[i][3])])
joins.append('JOIN %s ON %s' % (self.joins[i][0], on))
joins = ' '.join(joins)
# build where
where = ' AND '.join(where)
# build query
if len(where) > 0:
query = """SELECT %s.* FROM %s %s WHERE %s""" \
% (self.table, self.table, joins, where)
else:
query = """SELECT %s.* FROM %s""" % (self.table, self.table)
# process query
c = self.inst.cursor(self.inst.log)
if len(where) > 0:
c.execute(query, fields)
else:
c.execute(query)
rows = c.fetchall()
c.close()
if len(rows) == 0:
return None
records = []
for row in rows:
records.append(self.dbclass(db=self.inst, raw=row))
if len(records):
return records
else:
return None
class MythBE( FileOps ):
__doc__ = FileOps.__doc__+"""
getPendingRecordings() - returns a list of scheduled recordings
getScheduledRecordings()- returns a list of scheduled recordings
getUpcomingRecordings() - returns a list of scheduled recordings
getRecorderList() - returns a list of all recorder ids
getFreeRecorderList() - returns a list of free recorder ids
lockTuner() - requests a lock of a recorder
freeTuner() - requests an unlock of a recorder
getCheckfile() - returns the location of a recording
getExpiring() - returns a list of expiring recordings
getFreeSpace() - returns a list of FreeSpace objects
getFreeSpaceSummary() - returns a tuple of total and used space
getLastGuideData() - returns the last date of guide data
getLoad() - returns a tuple of load averages
getRecordings() - returns a list of all recordings
getSGFile() - returns information on a single file
getSGList() - returns lists of directories,
files, and sizes
getUptime() - returns system uptime in seconds
isActiveBackend() - determines whether backend is
currently active
isRecording - determinds whether recorder is
currently recording
walkSG() - walks a storage group tree, similarly
to os.walk(). returns a tuple of dirnames
and dictionary of filenames with sizes
"""
locked_tuners = []
def __del__(self):
self.freeTuner()
def getPendingRecordings(self):
"""
Returns a list of Program objects which are scheduled to be recorded.
"""
programs = []
res = self.backendCommand('QUERY_GETALLPENDING').split(BACKEND_SEP)
has_conflict = int(res.pop(0))
num_progs = int(res.pop(0))
for i in range(num_progs):
programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
+ PROGRAM_FIELDS], db=self.db))
return programs
def getScheduledRecordings(self):
"""
Returns a list of Program objects which are scheduled to be recorded.
"""
programs = []
res = self.backendCommand('QUERY_GETALLSCHEDULED').split(BACKEND_SEP)
num_progs = int(res.pop(0))
for i in range(num_progs):
programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS)
+ PROGRAM_FIELDS], db=self.db))
return programs
def getUpcomingRecordings(self):
"""
Returns a list of Program objects which are scheduled to be recorded.
Sorts the list by recording start time and only returns those with
record status of WillRecord.
"""
def sort_programs_by_starttime(x, y):
if x.starttime > y.starttime:
return 1
elif x.starttime == y.starttime:
return 0
else:
return -1
programs = []
res = self.getPendingRecordings()
for p in res:
if p.recstatus == p.WILLRECORD:
programs.append(p)
programs.sort(sort_programs_by_starttime)
return programs
def getRecorderList(self):
"""
Returns a list of recorders, or an empty list if none.
"""
recorders = []
c = self.db.cursor(self.log)
c.execute('SELECT cardid FROM capturecard')
row = c.fetchone()
while row is not None:
recorders.append(int(row[0]))
row = c.fetchone()
c.close()
return recorders
def getFreeRecorderList(self):
"""
Returns a list of free recorders, or an empty list if none.
"""
res = self.backendCommand('GET_FREE_RECORDER_LIST').split(BACKEND_SEP)
recorders = [int(d) for d in res]
return recorders
def lockTuner(self,id=None):
"""
Request a tuner be locked from use, optionally specifying which tuner
Returns a tuple of ID, video dev node, audio dev node, vbi dev node
Returns an ID of -2 if tuner is locked
-1 if no tuner could be found
"""
local = True
cmd = 'LOCK_TUNER'
if id is not None:
cmd += ' %d' % id
res = self.getRecorderDetails(id).hostname
if res != socket.gethostname():
local = False
res = ''
if local:
res = self.backendCommand(cmd).split(BACKEND_SEP)
else:
myth = MythTV(res)
res = myth.backendCommand(cmd).split(BACKEND_SEP)
myth.close()
res[0] = int(res[0])
if res[0] > 0:
self.locked_tuners.append(res[0])
return tuple(res[1:])
return res[0]
def freeTuner(self,id=None):
"""
Frees a requested tuner ID
If no ID given, free all tuners listed as used by this class instance
"""
def free(self,id):
res = self.getRecorderDetails(id).hostname
if res == socket.gethostname():
self.backendCommand('FREE_TUNER %d' % id)
else:
myth = MythTV(res)
myth.backendCommand('FREE_TUNER %d' % id)
myth.close()
if id is None:
for i in range(len(self.locked_tuners)):
free(self,self.locked_tuners.pop())
else:
try:
self.locked_tuners.remove(id)
except:
pass
free(self,id)
def getCurrentRecording(self, recorder):
"""
Returns a Program object for the current recorders recording.
"""
res = self.backendCommand('QUERY_RECORDER '+
BACKEND_SEP.join([str(recorder),'GET_CURRENT_RECORDING']))
return Program(res.split(BACKEND_SEP), db=self.db)
def isRecording(self, recorder):
"""
Returns a boolean as to whether the given recorder is recording.
"""
res = self.backendCommand('QUERY_RECORDER '+
BACKEND_SEP.join([str(recorder),'IS_RECORDING']))
if res == '1':
return True
else:
return False
def isActiveBackend(self, hostname):
"""
Returns a boolean as to whether the given host is an active backend
"""
res = self.backendCommand(BACKEND_SEP.join(\
['QUERY_IS_ACTIVE_BACKEND',hostname]))
if res == 'TRUE':
return True
else:
return False
def getRecording(self, chanid, starttime):
"""
Returns a Program object matching the channel id and start time
"""
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 getRecordings(self):
"""
Returns a list of all Program objects which have already recorded
"""
programs = []
res = self.backendCommand('QUERY_RECORDINGS Play').split(BACKEND_SEP)
num_progs = int(res.pop(0))
for i in range(num_progs):
programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
+ PROGRAM_FIELDS], db=self.db))
return programs
def getExpiring(self):
"""
Returns a tuple of all Program objects nearing expiration
"""
programs = []
res = self.backendCommand('QUERY_GETEXPIRING').split(BACKEND_SEP)
num_progs = int(res.pop(0))
for i in range(num_progs):
programs.append(Program(res[i * PROGRAM_FIELDS:(i*PROGRAM_FIELDS)
+ PROGRAM_FIELDS], db=self.db))
return programs
def getCheckfile(self,program):
"""
Returns location of recording in file system
"""
res = self.backendCommand(BACKEND_SEP.join(\
['QUERY_CHECKFILE','1',program.toString()]\
)).split(BACKEND_SEP)
if res[0] == 0:
return None
else:
return res[1]
def getFreeSpace(self,all=False):
"""
Returns a tuple of FreeSpace objects
"""
command = 'QUERY_FREE_SPACE'
if all:
command = 'QUERY_FREE_SPACE_LIST'
res = self.backendCommand(command).split(BACKEND_SEP)
dirs = []
for i in range(0,len(res)/10):
dirs.append(FreeSpace(res[i*10:i*10+10]))
return dirs
def getFreeSpaceSummary(self):
"""
Returns a tuple of total space (in KB) and used space (in KB)
"""
res = self.backendCommand('QUERY_FREE_SPACE_SUMMARY')\
.split(BACKEND_SEP)
return (self.joinInt(int(res[0]),int(res[1])),
self.joinInt(int(res[2]),int(res[3])))
def getLoad(self):
"""
Returns a tuple of the 1, 5, and 15 minute load averages
"""
res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP)
return (float(res[0]),float(res[1]),float(res[2]))
def getUptime(self):
"""
Returns machine uptime in seconds
"""
return self.backendCommand('QUERY_UPTIME')
def walkSG(self, host, sg, top=None):
"""
Performs similar to os.walk(), returning a tuple of tuples containing
(dirpath, dirnames, filenames).
'dirnames' is a tuple of all directories at the current level.
'filenames' is a dictionary, where the values are the file sizes.
"""
def walk(self, host, sg, root, path):
res = self.getSGList(host, sg, root+path+'/')
if res < 0:
return {}
dlist = list(res[0])
res = [dlist, dict(zip(res[1],res[2]))]
if path == '':
res = {'/':res}
else:
res = {path:res}
for d in dlist:
res.update(walk(self, host, sg, root, path+'/'+d))
return res
bases = self.getSGList(host, sg, '')
if (top == '/') or (top is None): top = ''
walked = {}
for base in bases:
res = walk(self, host, sg, base, top)
for d in res:
if d in walked:
walked[d][0] += res[d][0]
walked[d][1].update(res[d][1])
else:
walked[d] = res[d]
res = []
for key, val in walked.iteritems():
res.append((key, tuple(val[0]), val[1]))
res.sort()
return res
def getSGList(self,host,sg,path,filenamesonly=False):
"""
Returns a tuple of directories, files, and filesizes.
Two special modes
'filenamesonly' returns only filenames, no directories or size
empty path base directories returns base storage group paths
"""
if filenamesonly: path = '' # force empty path
res = self.backendCommand(BACKEND_SEP.join(\
['QUERY_SG_GETFILELIST',host,sg,path,
str(int(filenamesonly))]\
)).split(BACKEND_SEP)
if res[0] == 'EMPTY LIST':
return -1
if res[0] == 'SLAVE UNREACHABLE: ':
return -2
dirs = []
files = []
sizes = []
for entry in res:
if filenamesonly:
files.append(entry)
elif path == '':
type,name = entry.split('::')
dirs.append(name)
else:
type,name,size = entry.split('::')
if type == 'file':
files.append(name)
sizes.append(size)
if type == 'dir':
dirs.append(name)
if filenamesonly:
return files
elif path == '':
return dirs
else:
return (dirs, files, sizes)
def getSGFile(self,host,sg,path):
"""
Returns a tuple of last modification time and file size
"""
res = self.backendCommand(BACKEND_SEP.join(\
['QUERY_SG_FILEQUERY',host,sg,path])).split(BACKEND_SEP)
if res[0] == 'EMPTY LIST':
return -1
if res[0] == 'SLAVE UNREACHABLE: ':
return -2
return tuple(res[1:3])
def getLastGuideData(self):
"""
Returns the last dat for which guide data is available
On error, 0000-00-00 00:00 is returned
"""
return self.backendCommand('QUERY_GUIDEDATATHROUGH')
class MythTV( MythBE ):
# renamed to MythBE()
pass
class Frontend(object):
isConnected = False
socket = None
host = None
port = None
def __init__(self, host, port):
self.host = host
self.port = int(port)
self.connect()
self.disconnect()
def __del__(self):
if self.isConnected:
self.disconnect()
def __repr__(self):
return "<Frontend '%s@%d' at %s>" % (self.host,self.port,hex(id(self)))
def __str__(self):
return "%s@%d" % (self.host, self.port)
def close(self): self.__del__()
def connect(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10)
try:
self.socket.connect((self.host, self.port))
except:
raise MythFEError(MythError.FE_CONNECTION, self.host, self.port)
if re.search("MythFrontend Network Control.*",self.recv()) is None:
self.socket.close()
self.socket = None
raise MythFEError(MythError.FE_ANNOUNCE, self.host, self.port)
self.isConnected = True
def disconnect(self):
self.send("exit")
self.socket.close()
self.socket = None
self.isConnected = False
def send(self,command):
if not self.isConnected:
self.connect()
self.socket.send("%s\n" % command)
def recv(self):
curstr = ''
prompt = re.compile('([\r\n.]*)\r\n# ')
while not prompt.search(curstr):
try:
curstr += self.socket.recv(100)
except socket.error:
raise MythFEError(MythError.FE_CONNECTION, self.host, self.port)
except KeyboardInterrupt:
raise
return prompt.split(curstr)[0]
def sendJump(self,jumppoint):
"""
Sends jumppoint to frontend
"""
self.send("jump %s" % jumppoint)
if self.recv() == 'OK':
return 0
else:
return 1
def getJump(self):
"""
Returns a tuple containing available jumppoints
"""
self.send("help jump")
return re.findall('(\w+)[ ]+- ([\w /,]+)',self.recv())
def sendKey(self,key):
"""
Sends keycode to connected frontend
"""
self.send("key %s" % key)
if self.recv() == 'OK':
return 0
else:
return 1
def getKey(self):
"""
Returns a tuple containing available special keys
"""
self.send("help key")
return self.recv().split('\r\n')[4].split(', ')
def sendQuery(self,query):
"""
Returns query from connected frontend
"""
self.send("query %s" % query)
return self.recv()
def getQuery(self):
"""
Returns a tuple containing available queries
"""
self.send("help query")
return re.findall('query ([\w ]*\w+)[ \r\n]+- ([\w /,]+)', self.recv())
def sendPlay(self,play):
"""
Send playback command to connected frontend
"""
self.send("play %s" % play)
if self.recv() == 'OK':
return 0
else:
return 1
def getPlay(self):
"""
Returns a tuple containing available playback commands
"""
self.send("help play")
return re.findall('play ([\w -:]*\w+)[ \r\n]+- ([\w /:,\(\)]+)',
self.recv())
class MythDB( MythDBBase ):
__doc__ = MythDBBase.__doc__+"""
obj.searchRecorded() - return a list of matching Recorded objects
obj.getRecorded() - return a single matching Recorded object
obj.searchOldRecorded() - return a list of matching
OldRecorded objects
obj.searchJobs() - return a list of matching Job objects
obj.searchGuide() - return a list of matching Guide objects
obj.searchRecord() - return a list of matching Record rules
obj.getFrontends() - return a list of available Frontends
obj.getFrontend() - return a single Frontend object
obj.getChannels() - return a list of all channels
"""
@databaseSearch
def searchRecorded(self, init=False, key=None, value=None):
"""
obj.searchRecorded(**kwargs) -> list of Recorded objects
Supports the following keywords:
title, subtitle, chanid, starttime, progstart,
category, hostname, autoexpire, commflagged,
stars, recgroup, playgroup, duplicate, transcoded,
watched, storagegroup, category_type,
airdate, stereo, subtitled, hdtv, closecaptioned,
partnumber, parttotal, seriesid, showtype, programid,
manualid, generic, cast, livetv,
syndicatedepisodenumber
"""
if init:
# table and join descriptor
return ('recorded', Recorded, ('livetv',),
('recordedprogram','recorded',('chanid','starttime')),
('recordedcredits','recorded',('chanid','starttime')),
('people','recordedcredits',('person',)))
# local table matches
if key in ('title','subtitle','chanid','starttime','progstart',
'category','hostname','autoexpire','commflagged',
'stars','recgroup','playgroup','duplicate',
'transcoded','watched','storagegroup'):
return ('recorded.%s=%%s' % key, value, 0)
# recordedprogram matches
if key in ('category_type','airdate','stereo','subtitled','hdtv',
'closecaptioned','partnumber','parttotal','seriesid',
'showtype','syndicatedepisodenumber','programid',
'manualid','generic'):
return ('recordedprogram.%s=%%s' % key, value, 1)
if key == 'cast':
return ('people.name=%s', value, 2|4)
if key == 'livetv':
if value is None:
return ('recorded.recgroup!=%s', 'LiveTV', 0)
return None
@databaseSearch
def searchOldRecorded(self, init=False, key=None, value=None):
"""
obj.searchOldRecorded(**kwargs) -> list of OldRecorded objects
Supports the following keywords:
title, subtitle, chanid, starttime, endtime,
category, seriesid, programid, station, duplicate,
generic
"""
if init:
return ('oldrecorded', OldRecorded, ())
if key in ('title','subtitle','chanid','starttime','endtime',
'category','seriesid','programid','station',
'duplicate','generic'):
return ('oldrecorded.%s=%%s' % key, value, 0)
return None
@databaseSearch
def searchJobs(self, init=False, key=None, value=None):
"""
obj.searchJobs(**kwars) -> list of Job objects
Supports the following keywords:
recorded, starttime, type, status, hostname,
title, subtitle, flags, olderthan, newerthan
"""
if init:
return ('jobqueue', Job, (),
('recorded','jobqueue',('chanid','starttime')))
if key in ('chanid','starttime','type','status','hostname'):
return ('jobqueue.%s=%%s' % key, value, 0)
if key in ('title','subtitle'):
return ('recorded.%s=%%s' % key, value, 1)
if key == 'flags':
return ('jobqueue.flags&%s', value, 0)
if key == 'olderthan':
return ('jobqueue.inserttime>%s', value, 0)
if key == 'newerthan':
return ('jobqueue.inserttime<%s', value, 0)
return None
@databaseSearch
def searchGuide(self, init=False, key=None, value=None):
"""
obj.searchGuide(**args) -> list of Guide objects
Supports the following keywords:
chanid, starttime, endtime, title, subtitle,
category, airdate, stars, previouslyshown,
stereo, subtitled, hdtv, closecaptioned,
partnumber, parttotal, seriesid, originalairdate,
showtype, programid, generic, syndicatedepisodenumber
"""
if init:
return ('program', Guide, ())
if key in ('chanid','starttime','endtime','title','subtitle',
'category','airdate','stars','previouslyshown','stereo',
'subtitled','hdtv','closecaptioned','partnumber',
'parttotal','seriesid','originalairdate','showtype',
'syndicatedepisodenumber','programid','generic'):
return ('%s=%%s' % key, value, 0)
if key == 'startbefore':
return ('starttime<%s', value, 0)
if key == 'startafter':
return ('starttime>%s', value, 0)
if key == 'endbefore':
return ('endtime<%s', value, 0)
if key == 'endafter':
return ('endtime>%s', value, 0)
return None
@databaseSearch
def searchRecord(self, init=False, key=None, value=None):
"""
obj.searchRecord(**kwargs) -> list of Record objects
Supports the following keywords:
type, chanid, starttime, startdate, endtime
enddate, title, subtitle, category, profile
recgroup, station, seriesid, programid, playgroup
"""
if init:
return ('record', Record, ())
if key in ('type','chanid','starttime','startdate','endtime','enddate',
'title','subtitle','category','profile','recgroup',
'station','seriesid','programid','playgroup'):
return ('%s=%%s' % key, value, 0)
return None
def getFrontends(self):
"""
Returns a list of Frontend objects for accessible frontends
"""
cursor = self.cursor(self.log)
cursor.execute("""SELECT DISTINCT hostname
FROM settings
WHERE hostname IS NOT NULL
AND value='NetworkControlEnabled'
AND data=1""")
frontends = []
for fehost in cursor.fetchall():
try:
frontend = self.getFrontend(fehost[0])
frontends.append(frontend)
except:
print "%s is not a valid frontend" % fehost[0]
cursor.close()
return frontends
def getFrontend(self,host):
"""
Returns a Frontend object for the specified host
"""
port = self.settings[host].NetworkControlPort
return Frontend(host,port)
def getRecorded(self, title=None, subtitle=None, chanid=None,
starttime=None, progstart=None):
"""
Tries to find a recording matching the given information.
Returns a Recorded object.
"""
records = self.searchRecorded(title=title, subtitle=subtitle,\
chanid=chanid, starttime=starttime,\
progstart=progstart)
if records:
return records[0]
else:
return None
def getChannels(self):
"""
Returns a tuple of channel object defined in the database
"""
channels = []
c = self.cursor(self.log)
c.execute("""SELECT * FROM channel""")
for row in c.fetchall():
channels.append(Channel(db=self, raw=row))
c.close()
return channels
# wrappers for backwards compatibility of old functions
def getChannel(self,chanid):
return Channel(chanid, db=self)
def getGuideData(self, chanid, date):
return self.searchGuide(chanid=chanid,
custom=(('DATE(starttime)=%s',date),))
def getSetting(self, value, hostname=None):
if not hostname:
hostname = 'NULL'
return self.settings[hostname][value]
def setSetting(self, value, data, hostname=None):
if not hostname:
hostname = 'NULL'
self.settings[hostname][value] = data
class MythXML( MythXMLConn ):
"""
Provides convenient methods to access the backend XML server.
"""
def getServDesc(self):
"""
Returns a dictionary of valid pages, as well as input and output
arguments.
"""
tree = self._queryTree('GetServDesc')
acts = {}
for act in tree.find('actionList').getchildren():
actname = act.find('name').text
acts[actname] = {'in':[], 'out':[]}
for arg in act.find('argumentList').getchildren():
argname = arg.find('name').text
argdirec = arg.find('direction').text
acts[actname][argdirec].append(argname)
return acts
def getHosts(self):
"""Returns a list of unique hostnames found in the settings table."""
tree = self._queryTree('GetHosts')
return [child.text for child in tree.find('Hosts').getchildren()]
def getKeys(self):
"""Returns a list of unique keys found in the settings table."""
tree = self._queryTree('GetKeys')
return [child.text for child in tree.find('Keys').getchildren()]
def getSetting(self, key, hostname=None, default=None):
"""Retrieves a setting from the backend."""
args = {'Key':key}
if hostname:
args['HostName'] = hostname
if default:
args['Default'] = default
tree = self._queryTree('GetSetting', **args)
return tree.find('Values').find('Value').text
def getProgramGuide(self, starttime, endtime, startchan=None, numchan=None):
"""
Returns a list of Guide objects corresponding to the given time period.
"""
args = {'StartTime':starttime, 'EndTime':endtime, 'Details':1}
if startchan:
args['StartChanId'] = startchan
if numchan:
args['NumOfChannels'] = numchan
else:
args['NumOfChannels'] = 1
progs = []
tree = self._queryTree('GetProgramGuide', **args)
for chan in tree.find('ProgramGuide').find('Channels').getchildren():
chanid = int(chan.attrib['chanId'])
for guide in chan.getchildren():
progs.append(Guide(db=self.db, etree=(chanid, guide)))
return progs
def getProgramDetails(self, chanid, starttime):
"""
Returns a Program object for the matching show.
"""
args = {'ChanId': chanid, 'StartTime':starttime}
tree = self._queryTree('GetProgramDetails', **args)
prog = tree.find('ProgramDetails').find('Program')
return Program(db=self.db, etree=prog)
def getChannelIcon(self, chanid):
"""Returns channel icon as a data string"""
return self._query('GetChannelIcon', ChanId=chanid)
def getRecorded(self, descending=True):
"""
Returns a list of Program objects for recorded shows on the backend.
"""
tree = self._queryTree('GetRecorded', Descending=descending)
progs = []
for prog in tree.find('Recorded').find('Programs').getchildren():
progs.append(Program(db=self.db, etree=prog))
return progs
def getExpiring(self):
"""
Returns a list of Program objects for expiring shows on the backend.
"""
tree = self._queryTree('GetExpiring')
progs = []
for prog in tree.find('Expiring').find('Programs').getchildren():
progs.append(Program(db=self.db, etree=prog))
return progs
class MythVideo( MythDBBase ):
"""
Provides convenient methods to access the MythTV MythVideo database.
"""
def __init__(self,db=None):
"""
Initialise the MythDB connection.
"""
MythDBBase.__init__(self,db)
# check schema version
self._check_schema('mythvideo.DBSchemaVer',
MVSCHEMA_VERSION, 'MythVideo')
def scanStorageGroups(self, deleteold=True):
"""
obj.scanStorageGroups(deleteold=True) ->
tuple of (new videos, missing videos)
If deleteold is true, missing videos will be automatically
deleted from videometadata, as well as any matching
country, cast, genre or markup data.
"""
# pull available hosts and videos
groups = self.getStorageGroup(groupname='Videos')
curvids = self.searchVideos()
newvids = {}
# pull available types
c = self.cursor(self.log)
c.execute("""SELECT extension FROM videotypes WHERE f_ignore=False""")
extensions = [a[0] for a in c.fetchall()]
c.close()
for i in range(len(extensions)):
extensions[i] = extensions[i].lower()
# filter local videos, only work on SG content
for i in range(len(curvids)-1, -1, -1):
if curvids[i].host in ('', None):
del curvids[i]
# index by file path
curvids = dict([(curvids[i].filename, curvids[i]) \
for i in range(len(curvids))])
# loop through all storage groups on all hosts
for sg in groups:
if sg.hostname not in newvids:
newvids[sg.hostname] = []
try:
be = MythBE(backend=sg.hostname, db=self)
except:
# skip any offline backends
curvids = curvids.values()
for i in range(len(curvids)-1, -1, -1):
if curvids[i].host == sg.hostname:
del curvids[i]
curvids = dict([(curvids[i].filename, curvids[i]) \
for i in range(len(curvids))])
continue
folders = be.walkSG(sg.hostname, 'Videos', '/')
# loop through each folder
for sgfold in folders:
# loop through each file
for sgfile in sgfold[2]:
if sgfold[0] == '/':
tpath = sgfile
else:
tpath = sgfold[0][1:]+'/'+sgfile
# filter by extension
if tpath.rsplit('.',1)[-1].lower() not in extensions:
#print 'skipping: '+tpath
continue
# existing file in existing location
if tpath in curvids:
#print 'matching file: '+tpath
del curvids[tpath]
else:
newvids[sg.hostname].append(tpath)
# re-index by hash value
curvids = curvids.values()
curvids = dict([(curvids[i].hash, curvids[i]) \
for i in range(len(curvids))])
# loop through unmatched videos for missing files
newvidlist = []
for hostname in newvids:
be = MythBE(backend=hostname, db=self)
for tpath in newvids[hostname]:
thash = be.getHash(tpath, 'Videos')
if thash in curvids:
#print 'matching hash: '+tpath
curvids[thash].update({'filename':tpath})
del curvids[thash]
else:
vid = Video(db=self).fromFilename(tpath)
vid.host = hostname
vid.hash = thash
newvidlist.append(vid)
if deleteold:
for vid in curvids.values():
vid.delete()
return (newvidlist, curvids.values())
@databaseSearch
def searchVideos(self, init=False, key=None, value=None):
"""
obj.searchVideos(**kwargs) -> list of Video objects
Supports the following keywords:
title, subtitle, season, episode, host, directory, year, cast,
genre, country, category, insertedbefore, insertedafter, custom
"""
if init:
return ('videometadata', Video, (),
('videometadatacast','videometadata',
('idvideo',),('intid',)), #1
('videocast','videometadatacast',
('intid',),('idcast',)), #2
('videometadatagenre','videometadata',
('idvideo',),('intid',)), #4
('videogenre','videometadatagenre',
('intid',),('idgenre',)), #8
('videometadatacountry','videometadata',
('idvideo',),('intid',)), #16
('videocountry','videometadatacountry',
('intid',),('idcountry',)), #32
('videocategory','videometadata',
('intid',),('category',))) #64
if key in ('title','subtitle','season','episode','host',
'director','year'):
return('videometadata.%s=%%s' % key, value, 0)
vidref = {'cast':1|2, 'genre':4|8, 'country':16|32, 'category':64}
if key in vidref:
return ('video%s.%s=%%s' % (key, key), value, vidref[key])
if key == 'exactfile':
return ('videometadata.filename=%s', value, 0)
if key == 'file':
return ('videometadata.filename LIKE %s', '%'+value, 0)
if key == 'insertedbefore':
return ('videometadata.insertdate<%s', value, 0)
if key == 'insertedafter':
return ('videometadata.insertdate>%s', value, 0)
return None
def getVideo(self, **kwargs):
"""
Tries to find a video matching the given information.
Returns a Video object.
"""
videos = self.searchVideos(**kwargs)
if videos:
return videos[0]
else:
return None
# wrappers for backwards compatibility of old functions
def pruneMetadata(self):
self.scanStorageGroups()
def getTitleId(self, title, subtitle=None, episode=None,
array=False, host=None):
videos = self.searchVideos(title, subtitle, season, episode, host)
if videos:
if array:
res = []
for vid in videos:
res.append(vid.intid)
return res
else:
return videos[0].intid
else:
return None
def getMetadata(self, id):
return list(Video(id=id, db=self))
def getMetadataDictionary(self, id):
return Video(id=id, db=self).data
def setMetadata(self, data, id=None):
if id is None:
return Video(db=self).create(data)
else:
Video(id=id, db=self).update(data)
def getMetadataId(self, videopath, host=None):
video = self.getVideo(file=videopath, host=host)
if video is None:
return None
else:
return video.intid
def hasMetadata(self, videopath, host=None):
vid = self.getVideo(file=videopath, host=host, exactfile=True)
if vid is None:
return False
elif vid.inetref == 0:
return False
else:
return True
def rmMetadata(self, file=None, id=None, host=None):
if file:
self.getVideo(file=file, host=host, exactfile=True).delete()
elif id:
Video(id=id, db=self).delete()
def rtnVideoStorageGroup(self, host=None):
return self.getStorageGroup(groupname='Videos',hostname=host)
def getTableFieldNames(self, tablename):
return self.tablefields[tablename]
def setCast(self, cast_name, idvideo):
Video(id=idvideo, db=self).cast.add(cast_name)
# WARNING: no return value
def setGenres(self, genre_name, idvideo):
Video(id=idvideo, db=self).genre.add(genre_name)
# WARNING: no return value
def setCountry(self, country_name, idvideo):
Video(id=idvideo, db=self).country.add(country_name)
# WARNING: no return value
def cleanGenres(self, idvideo):
Video(id=idvideo, db=self).genre.clean()
def cleanCountry(self, idvideo):
Video(id=idvideo, db=self).country.clean()
def cleanCast(self, idvideo):
Video(id=idvideo, db=self).cast.clean()