2010-07-30 19:21:27 -07:00
|
|
|
# -*- 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):
|
2010-08-18 18:22:46 -07:00
|
|
|
res = self.getSGList(host, sg, root+path+'/')
|
|
|
|
if res < 0:
|
|
|
|
return {}
|
|
|
|
dlist = list(res[0])
|
|
|
|
res = [dlist, dict(zip(res[1],res[2]))]
|
2010-07-30 19:21:27 -07:00
|
|
|
if path == '':
|
|
|
|
res = {'/':res}
|
|
|
|
else:
|
|
|
|
res = {path:res}
|
2010-08-18 18:22:46 -07:00
|
|
|
for d in dlist:
|
2010-07-30 19:21:27 -07:00
|
|
|
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,
|
2010-08-18 18:22:46 -07:00
|
|
|
custom=(('DATE(starttime)=%s',date),))
|
2010-07-30 19:21:27 -07:00
|
|
|
|
|
|
|
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
|
2010-08-18 18:22:46 -07:00
|
|
|
if tpath.rsplit('.',1)[-1].lower() not in extensions:
|
2010-07-30 19:21:27 -07:00
|
|
|
#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()
|
|
|
|
|