[mythtvnz] Finding a gap in the MythTV Scheduler
Hadley Rich
mythtvnz@lists.linuxnut.co.nz
Sat, 19 May 2007 11:06:07 +1200
--Boundary-00=_fFjTG2alm0oTD6y
Content-Type: text/plain;
charset="iso-8859-15"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
On Sat, 19 May 2007 10:51:09 Steven Ellis wrote:
> Finally have a chance to try your code out, and sadly we base our build
> of 0.20-fixes. Any tips on getting it running?
>
> I created a file test.py as follows
>
> #!/usr/bin/python
> import MythTV
> m = MythTV.MythTV()
>
> upcoming = m.getUpcomingRecordings()
>
> print upcoming[0].title, upcoming[0].recstartts
>
>
> Then running it produces
>
> mythtv@mythtv:~$ ./test.py
> Traceback (most recent call last):
> File "./test.py", line 3, in ?
> m = MythTV.MythTV()
> File "/home/mythtv/MythTV.py", line 135, in __init__
> self.db = get_database_connection()
> NameError: global name 'get_database_connection' is not defined
>
>
> Got any pointers?
>
> Steve
Hmm, the guy that created find_meta.py adjusted it just the other day and
unfortunately it looks like it broke the MythTV class. I'll get it fixed in
SVN shortly. In the mean time I've attached a fixed version to this mail (I
hope a text attachment is OK). Ironically the adjustment that he made was to
make it work with .20-fixes :)
hads
--
http://nicegear.co.nz
New Zealand's VoIP Supplier
--Boundary-00=_fFjTG2alm0oTD6y
Content-Type: application/x-python;
name="MythTV.py"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
filename="MythTV.py"
#!/usr/bin/python
import logging
log = logging.getLogger('mythtv')
log.setLevel(logging.WARNING)
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(levelname)s - %(message)s'))
log.addHandler(ch)
import os
import sys
import socket
import shlex
import socket
import code
from datetime import datetime
try:
import MySQLdb
except:
log.critical("MySQLdb (python-mysqldb) is required but is not found.")
sys.exit(1)
RECSTATUS = {
'TunerBusy': -8,
'LowDiskSpace': -7,
'Cancelled': -6,
'Deleted': -5,
'Aborted': -4,
'Recorded': -3,
'Recording': -2,
'WillRecord': -1,
'Unknown': 0,
'DontRecord': 1,
'PreviousRecording': 2,
'CurrentRecording': 3,
'EarlierShowing': 4,
'TooManyRecordings': 5,
'NotListed': 6,
'Conflict': 7,
'LaterShowing': 8,
'Repeat': 9,
'Inactive': 10,
'NeverRecord': 11,
}
BACKEND_SEP = '[]:[]'
PROTO_VERSION = 34
PROGRAM_FIELDS = 43
class MythDB:
def __init__(self):
"""
A connection to the mythtv database
"""
config_files = [
'/usr/local/share/mythtv/mysql.txt',
'/usr/share/mythtv/mysql.txt',
'/usr/local/etc/mythtv/mysql.txt',
'/etc/mythtv/mysql.txt',
os.path.expanduser('~/.mythtv/mysql.txt'),
]
if 'MYTHCONFDIR' in os.environ:
config_locations.append('%s/mysql.txt' % os.environ['MYTHCONFDIR'])
found_config = False
for config_file in config_files:
try:
config = shlex.shlex(open(config_file))
except:
continue
token = config.get_token()
db_host = db_user = db_password = None
while token != config.eof and (db_host == None or db_user == None or db_password == None):
if token == "DBHostName":
if config.get_token() == "=":
db_host = config.get_token()
elif token == "DBUserName":
if config.get_token() == "=":
db_user = config.get_token()
elif token == "DBPassword":
if config.get_token() == "=":
db_password = config.get_token()
token = config.get_token()
log.debug('Using config %s' % config_file)
found_config = True
break
if not found_config:
raise "Unable to find MythTV configuration file"
self.db = MySQLdb.connect(user=db_user, host=db_host, passwd=db_password, db="mythconverg")
def getSetting(self, value, hostname=None):
"""
Returns the value for the given MythTV setting.
Returns None if the setting was not found. If multiple rows are
found (multiple hostnames), returns the value of the first one.
"""
log.debug('Looking for setting %s for host %s', value, hostname)
c = self.db.cursor()
if hostname is None:
c.execute("""
SELECT data
FROM settings
WHERE value LIKE(%s) AND hostname IS NULL LIMIT 1""",
(value,))
else:
c.execute("""
SELECT data
FROM settings
WHERE value LIKE(%s) AND hostname LIKE(%s) LIMIT 1""",
(value, hostname))
row = c.fetchone()
c.close()
if row:
return row[0]
else:
return None
def cursor(self):
return self.db.cursor()
class MythTV:
"""
A connection to MythTV backend
"""
def __init__(self, conn_type='Monitor'):
self.db = MythDB()
self.master_host = self.db.getSetting('MasterServerIP')
self.master_port = int(self.db.getSetting('MasterServerPort'))
if not self.master_host:
log.critical('Unable to find MasterServerIP in database')
sys.exit(1)
if not self.master_port:
log.critical('Unable to find MasterServerPort in database')
sys.exit(1)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10)
self.socket.connect((self.master_host, self.master_port))
res = self.backendCommand('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP)
if res[0] == 'REJECT':
log.critical('Backend has version %s and we speak version %s', res[1], PROTO_VERSION)
sys.exit(1)
res = self.backendCommand('ANN %s %s 0' % (conn_type, socket.gethostname()))
if res != 'OK':
log.critical('Unexpected answer to ANN command: %s', res)
def backendCommand(self, data):
"""
Sends a command via a socket to the mythbackend in
the format that it expects. Returns the result from
the backend
"""
def recv():
"""
Reads the data returned fomr the backend
"""
# The first 8 bytes of the response gives us the length
data = self.socket.recv(8)
try:
length = int(data)
except:
return ''
data = []
while length > 0:
chunk = self.socket.recv(length)
length = length - len(chunk)
data.append(chunk)
return ''.join(data)
command = '%-8d%s' % (len(data), data)
log.debug('Sending command: %s' % command)
self.socket.send(command)
return recv()
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))
log.debug('%s pending recordings', num_progs)
for i in range(num_progs):
programs.append(
Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
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))
log.debug('%s scheduled recordings', num_progs)
for i in range(num_progs):
programs.append(
Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) + PROGRAM_FIELDS]))
return programs
def getUpcomingRecordings(self):
"""
Returns a list of Program objects for programs which are actually
going to be recorded.
"""
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 == RECSTATUS['WillRecord']:
programs.append(p)
programs.sort(sort_programs_by_starttime)
return programs
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]
if recorders[0]:
return recorders
else:
return []
def getCurrentRecording(self, recorder):
res = self.backendCommand('QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % recorder)
return Program(res.split(BACKEND_SEP))
def isRecording(self, recorder):
res = self.backendCommand('QUERY_RECORDER %s[]:[]IS_RECORDING' % recorder)
if res == '1':
return True
else:
return False
class MythVideo:
def __init__(self):
self.db = MythDB()
def pruneMetadata(self):
c = self.db.cursor()
c.execute("""
SELECT intid, filename
FROM videometadata""")
row = c.fetchone()
while row is not None:
intid = row[0]
filename = row[1]
if not os.path.exists(filename):
log.info("%s not exist, removing metadata..." % filename)
c2 = self.db.cursor()
c2.execute("""DELETE FROM videometadata WHERE intid = %s""", (intid,))
c2.close()
row = c.fetchone()
c.close()
def getGenreId(self, genre_name):
"""
Find the id of the given genre from MythDB.
If the genre does not exist, insert it and return its id.
"""
c = self.db.cursor()
c.execute("SELECT intid FROM videocategory WHERE lower(category) = %s", (genre_name,))
row = c.fetchone()
c.close()
if row is not None:
return row[0]
# Insert a new genre.
c = self.db.cursor()
c.execute("INSERT INTO videocategory(category) VALUES (%s)", (genre_name.capitalize(),))
newid = c.lastrowid
c.close()
return newid
def getMetadataId(self, videopath):
"""
Finds the MythVideo metadata id for the given video path from the MythDB, if any.
Returns None if no metadata was found.
"""
c = self.db.cursor()
c.execute("""
SELECT intid
FROM videometadata
WHERE filename = %s""", (videopath,))
row = c.fetchone()
c.close()
if row is not None:
return row[0]
else:
return None
def getMetadata(self, id):
"""
Finds the MythVideo metadata for the given id from the MythDB, if any.
Returns None if no metadata was found.
"""
c = self.db.cursor()
c.execute("""
SELECT *
FROM videometadata
WHERE intid = %s""", (id,))
row = c.fetchone()
c.close()
if row is not None:
return row
else:
return None
def setMetadata(self, data, id=None):
c = self.db.cursor()
if id is None:
fields = ', '.join(data.keys())
format_string = ', '.join(['%s' for d in data.values()])
sql = "INSERT INTO videometadata(%s) VALUES(%s)" % (fields, format_string)
c.execute(sql, data.values())
intid = c.lastrowid
c.close()
return intid
else:
log.debug('Updating metadata for %s' % id)
format_string = ', '.join(['%s = %%s' % d for d in data])
sql = "UPDATE videometadata SET %s WHERE intid = %%s" % format_string
sql_values = data.values()
sql_values.append(id)
c.execute(sql, sql_values)
c.close()
class Program:
def __init__(self, data):
"""
Load the list of data into the object
"""
self.title = data[0]
self.subtitle = data[1]
self.description = data[2]
self.category = data[3]
try:
self.chanid = int(data[4])
except ValueError:
self.chanid = None
self.channum = data[5] #chanstr
self.callsign = data[6] #chansign
self.channame = data[7]
self.filename = data[8] #pathname
self.fs_high = data[9]
self.fs_low = data[10]
self.starttime = datetime.fromtimestamp(int(data[11])) # startts
self.endtime = datetime.fromtimestamp(int(data[12])) #endts
self.duplicate = int(data[13])
self.shareable = int(data[14])
self.findid = int(data[15])
self.hostname = data[16]
self.sourceid = int(data[17])
self.cardid = int(data[18])
self.inputid = int(data[19])
self.recpriority = int(data[20])
self.recstatus = int(data[21])
self.recordid = int(data[22])
self.rectype = data[23]
self.dupin = data[24]
self.dupmethod = data[25]
self.recstartts = datetime.fromtimestamp(int(data[26]))
self.recendts = datetime.fromtimestamp(int(data[27]))
self.repeat = int(data[28])
self.programflags = data[29]
self.recgroup = data[30]
self.commfree = int(data[31])
self.outputfilters = data[32]
self.seriesid = data[33]
self.programid = data[34]
self.lastmodified = data[35]
self.stars = float(data[36])
self.airdate = data[37]
self.hasairdate = int(data[38])
self.playgroup = data[39]
self.recpriority2 = int(data[40])
self.parentid = data[41]
self.storagegroup = data[42]
if __name__ == '__main__':
banner = "'m' is a MythTV instance."
try:
import readline, rlcompleter
except:
pass
else:
readline.parse_and_bind("tab: complete")
banner = banner + " TAB completion is available."
m = MythTV()
namespace = globals().copy()
namespace.update(locals())
code.InteractiveConsole(namespace).interact(banner)
--Boundary-00=_fFjTG2alm0oTD6y--