Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1248 lines (1088 sloc) 47.1 KB
# -*- coding: utf-8 -*-
"""
Provides base classes for accessing MythTV
"""
from MythTV.static import *
from MythTV.exceptions import *
from MythTV.logging import MythLog
from MythTV.connections import FEConnection, XMLConnection, BEEventConnection
from MythTV.utility import databaseSearch, datetime, check_ipv6, _donothing
from MythTV.database import DBCache, DBData
from MythTV.system import SystemEvent
from MythTV.mythproto import BECache, FileOps, Program, FreeSpace, EventLock
from MythTV.dataheap import *
from datetime import timedelta
from weakref import proxy
try:
from urllib import urlopen
except ImportError:
from urllib.request import urlopen
import re
class CaptureCard( DBData ):
pass
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
getConflictedRecordings() - returns a list of conflicting 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()
@FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True)
def getPendingRecordings(self, pg):
"""
Returns a list of Program objects which are scheduled to be recorded.
"""
return pg
@FileOps._ProgramQuery('QUERY_GETALLSCHEDULED', sorted=True)
def getScheduledRecordings(self, pg):
"""
Returns a list of Program objects which are scheduled to be recorded.
"""
return pg
@FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True,
recstatus=Program.rsWillRecord)
def getUpcomingRecordings(self, pg):
"""
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.
"""
return pg
@FileOps._ProgramQuery('QUERY_GETALLPENDING', header_length=1, sorted=True,
recstatus=Program.rsConflict)
def getConflictedRecordings(self, pg):
"""
Retuns a list of Program objects subject to conflicts in the schedule.
"""
return pg
def getRecorderList(self):
"""
Returns a list of recorders, or an empty list if none.
"""
recorders = []
with self.db.cursor(self.log) as cursor:
cursor.execute('SELECT cardid FROM capturecard')
for row in cursor:
recorders.append(int(row[0]))
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
"""
cmd = 'LOCK_TUNER'
be = self
if id is not None:
card = None
try:
# pull information from database to confirm existance
card = CaptureCard(id)
except MythError:
raise MythError("Capture card %s not found" % id)
cmd += ' %d' % id
if card.hostname != be.hostname:
# connect to slave backend if needed
be = MythBE(card.hostname, db=self.db)
res = be.backendCommand(cmd).split(BACKEND_SEP)
err = int(res[0])
if err > 0:
# success, store tuner and return device nodes
self.locked_tuners[err] = be.hostname
return tuple(res[1:])
# return failure mode
return err
def freeTuner(self,id=None):
"""
Frees a requested tuner ID
If no ID given, free all tuners listed as used by this class instance
"""
tunerlist = {}
if id is not None:
id = int(id)
if id in self.locked_tuners:
# tuner is known, pop from list
tunerlist[id] = self.locked_tuners.pop(id)
else:
# tuner is not known, find hostname
try:
card = CaptureCard(id)
except MythError:
raise MythError("Capture card %s not found" % id)
tunerlist[id] = card.hostname
else:
# use the stored list
tunerlist = self.locked_tuners
hosts = {self.hostname:self}
while True:
try:
# get a tuner
id, host = tunerlist.popitem()
# get the backend connection
be = None
if host in hosts:
be = hosts[host]
else:
be = MythBE(host, db=self.db)
hosts[host] = be
# unlock the tuner
be.backendCommand('FREE_TUNER %d' % id)
except KeyError:
# out of tuners
break
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
@FileOps._ProgramQuery('QUERY_RECORDINGS Ascending', sorted=True)
def getRecordings(self, pg):
"""
Returns a list of all Program objects which have already recorded
"""
return pg
@FileOps._ProgramQuery('QUERY_GETEXPIRING', sorted=True)
def getExpiring(self, pg):
"""
Returns a tuple of all Program objects nearing expiration
"""
return pg
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)
l = len(FreeSpace._field_order)
for i in xrange(len(res)/l):
yield FreeSpace(res[i*l:i*l+l])
def getFreeSpaceSummary(self):
"""
Returns a tuple of total space (in KB) and used space (in KB)
"""
res = [int(r) for r in self.backendCommand('QUERY_FREE_SPACE_SUMMARY')\
.split(BACKEND_SEP)]
return (res[0], res[1])
def getLoad(self):
"""
Returns a tuple of the 1, 5, and 15 minute load averages
"""
return [float(r) for r in self.backendCommand('QUERY_LOAD')\
.split(BACKEND_SEP)]
def getUptime(self):
"""
Returns machine uptime in seconds
"""
return timedelta(0, int(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:
se = entry.split('::')
if se[0] == 'file':
files.append(se[1])
sizes.append(se[2])
elif se[0] == 'dir':
dirs.append(se[1])
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
"""
try:
return datetime.duck(self.backendCommand('QUERY_GUIDEDATATHROUGH'))
except ValueError:
return None
def clearSettings(self):
"""
Triggers an event to clear the settings cache on all active systems.
"""
self.backendCommand('MESSAGE%sCLEAR_SETTINGS_CACHE' % BACKEND_SEP)
self.db.settings.clear()
class BEEventMonitor( BECache ):
def __init__(self, backend=None, blockshutdown=False,
systemevents=False, db=None):
self.systemevents = systemevents
super(BEEventMonitor, self).__init__(backend, blockshutdown, True, db)
def _listhandlers(self):
return [self.eventMonitor]
def _neweventconn(self):
return BEEventConnection(self.host, self.port, self.db.gethostname(),
level = (2,1)[self.systemevents])
def eventMonitor(self, event=None):
if event is None:
return re.compile('BACKEND_MESSAGE')
self.log(MythLog.ALL, MythLog.INFO, event)
class MythSystemEvent( BECache ):
class systemeventhandler( object ):
# decorator class for system events
bs = BACKEND_SEP.replace('[','\[').replace(']','\]')
re_process = re.compile(bs.join([
'BACKEND_MESSAGE',
'SYSTEM_EVENT (?P<event>[A-Z_]*)'
'( HOSTNAME (?P<hostname>[a-zA-Z0-9_\.]*))?'
'( SENDER (?P<sender>[a-zA-Z0-9_\.]*))?'
'( CARDID (?P<cardid>[0-9]*))?'
'( CHANID (?P<chanid>[0-9]*))?'
'( STARTTIME (?P<starttime>[0-9-]*T[0-9-]))?'
'( SECS (?P<secs>[0-9]*))?',
'empty']))
def __init__(self, func):
self.func = func
self.__doc__ = self.func.__doc__
self.__name__ = self.func.__name__
self.__module__ = self.func.__module__
self.filter = None
self.re_search = re.compile('BACKEND_MESSAGE%sSYSTEM_EVENT %s' %\
(self.bs, self.__name__.upper()))
self.generic = (self.__name__ == '_generic_handler')
if self.generic:
self.re_search = re.compile(\
'BACKEND_MESSAGE%sSYSTEM_EVENT' % self.bs)
def __get__(self, inst, own):
self.inst = inst
return self
def __call__(self, event=None):
if event is None:
return self.re_search
# basic string processing
match = self.re_process.match(event)
event = {}
for a in ('event','hostname','sender','cardid','secs'):
if match.group(a) is not None:
event[a] = match.group(a)
# event filter for generic handler
if self.generic:
if self.filter is None:
self.filter = [f.__name__.upper() \
for f in self.inst._events]
if event['event'] in self.filter:
return
# pull program information
if (match.group('chanid') is not None) and \
(match.group('starttime') is not None):
be = MythBE(self.inst.hostname)
event['program'] = be.getRecording(\
(match.group('chanid'),match.group('starttime')))
self.func(self.inst, event)
def _listhandlers(self):
return []
def __init__(self, backend=None, blockshutdown=False, db=None,
enablehandler=True):
super(MythSystemEvent, self).__init__(backend, blockshutdown, True, db)
if enablehandler:
self._events.append(self._generic_handler)
self.registerevent(self._generic_handler)
def _neweventconn(self):
return BEEventConnection(self.host, self.port, self.db.gethostname(), 3)
@systemeventhandler
def _generic_handler(self, event):
SystemEvent(event['event'], inst.db).command(event)
class Frontend( FEConnection ):
_db = None
class _Jump( object ):
def __str__(self): return str(self.list())
def __repr__(self): return str(self)
def __init__(self, parent):
self._parent = proxy(parent)
self._points = {}
def _populate(self):
self._populate = _donothing
self._points = dict(self._parent.send('jump'))
def __getitem__(self, key):
self._populate()
if key in self._points:
return self._parent.send('jump', key)
else:
return False
def __getattr__(self, key):
self._populate()
if key in self.__dict__:
return self.__dict__[key]
return self.__getitem__(key)
def dict(self):
self._populate()
return self._points
def list(self):
self._populate()
return self._points.items()
class _Key( object ):
_keymap = { 9:'tab', 10:'enter', 27:'escape',
32:'space', 92:'backslash', 127:'backspace',
258:'down', 259:'up', 260:'left',
261:'right', 265:'f1', 266:'f2',
267:'f3', 268:'f4', 269:'f5',
270:'f6', 271:'f7', 272:'f8',
273:'f9', 274:'f10', 275:'f11',
276:'f12', 330:'delete', 331:'insert',
338:'pagedown', 339:'pageup'}
_alnum = [chr(i) for i in list(range(48,58))+list(range(65,91))+list(range(97,123))]
def __str__(self): return str(self.list())
def __repr__(self): return str(self)
def __init__(self, parent):
self._parent = proxy(parent)
self._keys = []
def _populate(self):
self._populate = _donothing
self._keys = self._parent.send('key')
def _sendLiteral(self, key):
if (key in self._keys) or (key in self._alnum):
return self._parent.send('key', key)
return False
def _sendOrdinal(self, key):
try:
key = int(key)
if key in self._keymap:
key = self._keymap[key]
else:
key = chr(key)
return self._sendLiteral(key)
except ValueError:
return False
def __getitem__(self, key):
self._populate()
if self._sendOrdinal(key):
return True
elif self._sendLiteral(key):
return True
else:
return False
def __getattr__(self, key):
self._populate()
if key in self.__dict__:
return self.__dict__[key]
return self._sendLiteral(key)
def list(self):
self._populate()
return self._keys
def __init__(self, *args, **kwargs):
FEConnection.__init__(self, *args, **kwargs)
self.jump = self._Jump(self)
self.key = self._Key(self)
def sendQuery(self,query): return self.send('query', query)
def getQuery(self): return self.send('query')
def sendPlay(self,play): return self.send('play', play)
def getPlay(self): return self.send('play')
def play(self, media):
"""Plays selected media on frontend."""
return media._playOnFe(self)
def getLoad(self):
"""Returns tuple of 1/5/15 load averages."""
return tuple([float(l) for l in self.sendQuery('load').split()])
def getUptime(self):
"""Returns timedelta of uptime."""
return timedelta(0, int(self.sendQuery('uptime')))
def getTime(self):
"""Returns current time on frontend."""
return datetime.fromIso(self.sendQuery('time'))
def getMemory(self):
"""Returns free and total, physical and swap memory."""
return dict(zip(('totalmem','freemem','totalswap','freeswap'),
[int(m) for m in self.sendQuery('memstats').split()]))
def getScreenShot(self):
"""Returns PNG image of the frontend."""
fd = urlopen('http://%s:6547/MythFE/GetScreenShot' % self.host)
img = fd.read()
fd.close()
return img
class MythDB( DBCache ):
__doc__ = DBCache.__doc__+"""
obj.searchRecorded() - return a list of matching Recorded objects
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
"""
@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, basename,
syndicatedepisodenumber, olderthan, newerthan,
inetref, season, episode
Multiple keywords can be chained as such:
obj.searchRecorded(title='Title', commflagged=False)
"""
if init:
# table and join descriptor
init.table = 'recorded'
init.handler = Recorded
init.require = ('livetv',)
init.joins = (init.Join(table='recordedprogram',
tableto='recorded',
fieldsfrom=('chanid','starttime'),
fieldsto=('chanid','progstart')),
init.Join(table='recordedcredits',
tableto='recorded',
fieldsfrom=('chanid','starttime'),
fieldsto=('chanid','progstart')),
init.Join(table='people',
tableto='recordedcredits',
fields=('person',)))
return None
# local table matches
if key in ('title','subtitle','chanid',
'category','hostname','autoexpire','commflagged',
'stars','recgroup','playgroup','duplicate',
'transcoded','watched','storagegroup','basename',
'inetref','season','episode'):
return ('recorded.%s=%%s' % key, value, 0)
# time matches
if key in ('starttime','endtime','progstart','progend'):
return ('recorded.%s=?' % key, datetime.duck(value), 0)
if key == 'olderthan':
return ('recorded.starttime<?', datetime.duck(value), 0)
if key == 'newerthan':
return ('recorded.starttime>?', datetime.duck(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=?' % key, value, 1)
if key == 'cast':
return ('people.name', 'recordedcredits', 4, 1)
if key == 'livetv':
if (value is None) or (value == False):
return ('recorded.recgroup!=?', 'LiveTV', 0)
return ()
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, recstatus, inetref, season, episode
"""
if init:
init.table = 'oldrecorded'
init.handler = OldRecorded
return None
if key in ('title','subtitle','chanid',
'category','seriesid','programid','station',
'duplicate','generic','recstatus','inetref',
'season','episode'):
return ('oldrecorded.%s=?' % key, value, 0)
# time matches
if key in ('starttime','endtime'):
return ('oldrecorded.%s=?' % key, datetime.duck(value), 0)
return None
@databaseSearch
def searchArtwork(self, init=False, key=None, value=None):
"""
obj.searchArtwork(**kwargs) -> list of RecordedArtwork objects
Supports the following keywords:
inetref, season, host, chanid, starttime
title, subtitle
"""
if init:
# table and join descriptor
init.table = 'recordedartwork'
init.handler = RecordedArtwork
init.joins = (init.Join(table='recorded',
tableto='recordedartwork',
fieldsfrom=('inetref','season','hostname'),
fieldsto=('inetref','season','host')),)
return None
if key in ('inetref', 'season', 'host'):
return ('recordedartwork.%s=?' % key, value, 0)
if key in ('chanid', 'title', 'subtitle'):
return ('recorded.%s=?' % key, value, 1)
if key == 'starttime':
return ('recorded.%s=?' % key, datetime.duck(value), 1)
return None
@databaseSearch
def searchJobs(self, init=False, key=None, value=None):
"""
obj.searchJobs(**kwars) -> list of Job objects
Supports the following keywords:
chanid, starttime, type, status, hostname,
title, subtitle, flags, olderthan, newerthan
"""
if init:
init.table = 'jobqueue'
init.handler = Job
init.joins = (init.Join(table='recorded',
tableto='jobqueue',
fields=('chanid','starttime')),)
return None
if key in ('chanid','type','status','hostname'):
return ('jobqueue.%s=?' % key, value, 0)
if key in ('title','subtitle'):
return ('recorded.%s=?' % key, value, 1)
if key == 'flags':
return ('jobqueue.flags&?', value, 0)
if key == 'starttime':
return ('jobqueue.starttime=?', datetime.duck(value), 0)
if key == 'olderthan':
return ('jobqueue.inserttime>?', datetime.duck(value), 0)
if key == 'newerthan':
return ('jobqueue.inserttime<?', datetime.duck(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,
ondate, cast, startbefore,startafter
endbefore, endafter, dayofweek, weekday
first, last, callsign, commfree
channelgroup, videosource,
genre, rating, cast, fuzzytitle
fuzzysubtitle, fuzzydescription,
fuzzyprogramid, beforedate, afterdate,
"""
if init:
init.table = 'program'
init.handler = Guide
init.joins = (init.Join(table='credits',
tableto='program',
fields=('chanid','starttime')),
init.Join(table='people',
tableto='credits',
fields=('person',)),
init.Join(table='channel',
tableto='program',
fields=('chanid',)),
init.Join(table='channelgroup',
tableto='program',
fields=('chanid',)),
init.Join(table='channelgroupnames',
tableto='channelgroup',
fields=('grpid',)),
init.Join(table='videosource',
tableto='channel',
fields=('sourceid',)),
init.Join(table='programgenres',
tableto='program',
fields=('chanid','starttime')),
init.Join(table='programrating',
tableto='program',
fields=('chanid','starttime')))
return None
if key in ('chanid','title','subtitle',
'category','airdate','stars','previouslyshown','stereo',
'subtitled','hdtv','closecaptioned','partnumber',
'parttotal','seriesid','originalairdate','showtype',
'syndicatedepisodenumber','programid','generic',
'category_type'):
return ('program.%s=?' % key, value, 0)
if key in ('starttime','endtime'):
return ('program.%s=?' % key, datetime.duck(value), 0)
if key == 'dayofweek':
return ('DAYNAME(program.starttime)=?', value, 0)
if key == 'weekday':
return ('WEEKDAY(program.starttime)<?', 5, 0)
if key in ('first', 'last'):
return ('program.%s=?' % key, 1, 0)
if key == 'callsign':
return ('channel.callsign=?', value, 4)
if key == 'commfree':
return ('channel.commmethod=?', -2, 4)
if key == 'channelgroup':
return ('channelgroupnames.name=?', value, 24)
if key == 'videosource':
try:
value = int(value)
return ('channel.sourceid=?', value, 4)
except:
return ('videosource.name=?', value, 36)
if key == 'genre':
return ('programgenres.genre=?', value, 64)
if key == 'rating':
return ('programrating.rating=?', value, 128)
if key == 'cast':
return ('people.name', 'credits', 2, 0)
if key.startswith('fuzzy'):
if key[5:] in ('title', 'subtitle', 'description', 'programid'):
return ('program.%s LIKE ?' % key[5:], '%'+value+'%', 0)
if key[5:] == 'callsign':
return ('channel.callsign LIKE ?', '%'+value+'%', 4)
if key.endswith('date'):
prefix = {'on':'=', 'before':'<', 'after':'>'}
if key[:-4] in prefix:
return ('DATE(program.starttime){0}?'.format(prefix[key[:-4]]),
value, 0)
if key == 'startbefore':
return ('program.starttime<?', datetime.duck(value), 0)
if key == 'startafter':
return ('program.starttime>?', datetime.duck(value), 0)
if key == 'endbefore':
return ('program.endtime<?', datetime.duck(value), 0)
if key == 'endafter':
return ('program.endtime>?', datetime.duck(value), 0)
return None
def makePowerRule(self, ruletitle='unnamed (Power Search',
type=RECTYPE.kAllRecord, **kwargs):
where, args, join = self.searchGuide.parseInp(kwargs)
where = ' AND '.join(where)
join = self.searchGuide.buildJoin(join)
return Record.fromPowerRule(title=ruletitle, type=type, where=where,
args=args, join=join, db=self)
@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,
inetref
"""
if init:
init.table = 'record'
init.handler = Record
return None
if key in ('type','chanid','starttime','startdate','endtime','enddate',
'title','subtitle','category','profile','recgroup',
'station','seriesid','programid','playgroup','inetref'):
return ('%s=?' % key, value, 0)
return None
@databaseSearch
def searchInternetContent(self, init=False, key=None, value=None):
"""
obj.searchInternetContent(**kwargs) -> list of content
Supports the following keywords:
feedtitle, title, subtitle, season, episode,
url, type, author, rating, player,
width, height, language, podcast, downloadable,
ondate, olderthan, newerthan, longerthan, shorterthan,
country, description
"""
if init:
init.table = 'internetcontentarticles'
init.handler = InternetContentArticles
return None
if key in ('feedtitle','title','subtitle','season','episode','url',
'type','author','rating','player','width','height',
'language','podcast','downloadable', 'description'):
return ('%s=?' % key, value, 0)
if key == 'ondate':
return ('DATE(date)=?', value, 0)
if key == 'olderthan':
return ('date>?', value, 0)
if key == 'newerthan':
return ('date<?', value, 0)
if key == 'longerthan':
return ('time<?', value, 0)
if key == 'shorterthan':
return ('time>?', value, 0)
if key == 'country':
return ('countries LIKE ?', '%'+value+'%', 0)
def getFrontends(self):
"""
Returns a list of Frontend objects for accessible frontends
"""
with self.cursor(self.log) as cursor:
cursor.execute("""SELECT DISTINCT hostname
FROM settings
WHERE hostname IS NOT NULL
AND value='NetworkControlEnabled'
AND data=1""")
frontends = [(fe[0], self.settings[fe[0]].NetworkControlPort) \
for fe in cursor]
return Frontend.testList(frontends)
def getFrontend(self,host):
"""
Returns a Frontend object for the specified host
"""
port = self.settings[host].NetworkControlPort
return Frontend(host,port)
def scanVideos(self):
"""
obj.scanVideo() --> list of new, moved, and deleted Videos
"""
startvids = dict([(vid.intid, vid) for vid in Video.getAllEntries(db=self)])
be = MythBE(db=self)
r = re.compile(re.escape(BACKEND_SEP).\
join(['BACKEND_MESSAGE',
'(VIDEO_LIST(_NO)?_CHANGE)',
'(.*)']))
lock = EventLock(r, db=self)
be.backendCommand("SCAN_VIDEOS")
lock.wait()
newvids = []
movvids = []
oldvids = []
match = r.match(lock.event)
if match.group(1) == "VIDEO_LIST_CHANGE":
for entry in match.group(3).split(BACKEND_SEP):
type,intid = entry.split('::')
intid = int(intid)
if type == 'added':
newvids.append(Video(intid))
elif type == 'moved':
v = startvids[intid]
v._pull()
movvids.append(v)
elif type == 'deleted':
oldvids.append(startvids[intid])
return newvids,movvids,oldvids
@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, director, year, cast,
genre, country, category, insertedbefore, insertedafter, custom
"""
if init:
init.table = 'videometadata'
init.handler = Video
init.joins = (init.Join(table='videometadatacast',
tableto='videometadata',
fieldsfrom=('idvideo',),
fieldsto=('intid',)),
init.Join(table='videocast',
tableto='videometadatacast',
fieldsfrom=('intid',),
fieldsto=('idcast',)),
init.Join(table='videometadatagenre',
tableto='videometadata',
fieldsfrom=('idvideo',),
fieldsto=('intid',)),
init.Join(table='videogenre',
tableto='videometadatagenre',
fieldsfrom=('intid',),
fieldsto=('idgenre',)),
init.Join(table='videometadatacountry',
tableto='videometadata',
fieldsfrom=('idvideo',),
fieldsto=('intid',)),
init.Join(table='videocountry',
tableto='videometadatacountry',
fieldsfrom=('intid',),
fieldsto=('idcountry',)),
init.Join(table='videocategory',
tableto='videometadata',
fieldsfrom=('intid',),
fieldsto=('category',)))
return None
if key in ('title','subtitle','season','episode','host',
'director','year'):
return('videometadata.%s=?' % key, value, 0)
vidref = {'cast':(2,0), 'genre':(8,2), 'country':(32,4)}
if key in vidref:
return ('video%s.%s' % (key,key),
'videometadata%s' % key,
vidref[key][0],
vidref[key][1])
if key == 'category':
return ('videocategory.%s=?' % key, value, 64)
if key == 'exactfile':
return ('videometadata.filename=?', value, 0)
if key == 'file':
return ('videometadata.filename LIKE ?', '%'+value, 0)
if key == 'insertedbefore':
return ('videometadata.insertdate<?', value, 0)
if key == 'insertedafter':
return ('videometadata.insertdate>?', value, 0)
return None
class MythVideo( MythDB ):
"""legacy - do not use"""
def scanStorageGroups(self, delete=True):
"""legacy - do not use"""
added, moved, deleted = scanVideos()
return (added, deleted)
class MythXML( XMLConnection ):
"""
Provides convenient methods to access the backend XML server.
"""
def __init__(self, backend=None, port=None, db=None):
if backend and port:
XMLConnection.__init__(self, backend, port)
self.db = db
return
self.db = DBCache(db)
self.log = MythLog('Python XML Connection')
if backend is None:
# use master backend
backend = self.db.settings.NULL.MasterServerIP
if re.match('(?:\d{1,3}\.){3}\d{1,3}',backend) or \
check_ipv6(backend):
# process ip address
host = self.db._gethostfromaddr(backend)
self.host = backend
self.port = int(self.db.settings[host].BackendStatusPort)
else:
# assume given a hostname
self.host = backend
self.port = int(self.db.settings[self.host].BackendStatusPort)
if not self.port:
# try a truncated hostname
self.host = backend.split('.')[0]
self.port = int(self.db.setting[self.host].BackendStatusPort)
if not self.port:
raise MythDBError(MythError.DB_SETTING,
backend+': BackendStatusPort')
def getHosts(self):
"""Returns a list of unique hostnames found in the settings table."""
return self._request('Myth/GetHosts')\
.readJSON()['StringList']
def getKeys(self):
"""Returns a list of unique keys found in the settings table."""
return self._request('Myth/GetKeys')\
.readJSON()['StringList']
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
return self._request('Myth/GetSetting', **args)\
.readJSON()['SettingList']['Settings'][0]['Value']
def getProgramGuide(self, starttime, endtime, startchan, numchan=None):
"""
Returns a list of Guide objects corresponding to the given time period.
"""
starttime = datetime.duck(starttime)
endtime = datetime.duck(endtime)
args = {'StartTime':starttime.utcisoformat().rsplit('.',1)[0],
'EndTime':endtime.utcisoformat().rsplit('.',1)[0],
'StartChanId':startchan, 'Details':1}
if numchan:
args['NumOfChannels'] = numchan
else:
args['NumOfChannels'] = 1
dat = self._request('Guide/GetProgramGuide', **args).readJSON()
for chan in dat['ProgramGuide']['Channels']:
for prog in chan['Programs']:
prog['ChanId'] = chan['ChanId']
yield Guide.fromJSON(prog, self.db)
def getProgramDetails(self, chanid, starttime):
"""
Returns a Program object for the matching show.
"""
starttime = datetime.duck(starttime)
args = {'ChanId': chanid, 'StartTime': starttime.utcisoformat()}
return Program.fromJSON(
self._request('Guide/GetProgramDetails', **args)\
.readJSON()['Program'],
db=self.db)
def getChannelIcon(self, chanid):
"""Returns channel icon as a data string"""
return self._request('Guide/GetChannelIcon', ChanId=chanid).read()
def getRecorded(self, descending=True):
"""
Returns a list of Program objects for recorded shows on the backend.
"""
for prog in self._request('Dvr/GetRecorded', Descending=descending)\
.readJSON()['ProgramList']['Programs']:
yield Program.fromJSON(prog, self.db)
def getExpiring(self):
"""
Returns a list of Program objects for expiring shows on the backend.
"""
for prog in self._request('Dvr/GetExpiringList')\
.readJSON()['ProgramList']['Programs']:
yield Program.fromJSON(prog, self.db)
# def getInternetSources(self):
# for grabber in self._queryTree('GetInternetSources').\
# find('InternetContent').findall('grabber'):
# yield InternetSource.fromEtree(grabber, self)
def getInternetContentUrl(self, grabber, videocode):
return "mythflash://%s:%s/InternetContent/GetInternetContent?Grabber=%s&videocode=%s" \
% (self.host, self.port, grabber, videocode)
def getPreviewImage(self, chanid, starttime, width=None, \
height=None, secsin=None):
starttime = datetime.duck(starttime)
args = {'ChanId':chanid, 'StartTime':starttime.utcisoformat()}
if width: args['Width'] = width
if height: args['Height'] = height
if secsin: args['SecsIn'] = secsin
return self._request('Content/GetPreviewImage', **args).read()
class MythMusic( MusicSchema, DBCache ):
"""
Provides convenient methods to access the MythTV MythMusic database.
"""
@databaseSearch
def searchMusic(self, init=False, key=None, value=None):
"""
obj.searchMusic(**kwargs) -> iterable of Song objects
Supports the following keywords:
name, track, disc_number, artist, album, year
genre, rating, format, sample_rate, bitrate
"""
if init:
init.table = 'music_songs'
init.handler = Song
init.joins = (init.Join(table='music_artists',
tableto='music_songs',
fields=('artist_id',)),
init.Join(table='music_albums',
tableto='music_songs',
fields=('album_id',)),
init.Join(table='music_genres',
tableto='music_songs',
fields=('genre_id',)))
if key in ('name','track','disc_number','rating',
'format','sample_rate','bitrate', 'hostname'):
return ('music_songs.%s=%%s' % key, value, 0)
if key == 'artist':
return ('music_artists.artist_name=%s', value, 1)
if key == 'album':
return ('music_albums.album_name=%s', value, 2)
if key == 'year':
return ('music_albums.year=%s', value, 2)
if key == 'genre':
return ('music_genres.genre=%s', value, 4)