#!/usr/bin/python2.4 # This script requires that mplayer be installed... # Copyright (C) Michael Still (mikal@stillhq.com) 2006, 2007 # Released under the terms of the GNU GPL # Latest source is always at http://www.stillhq.com/mythtv/mythnettv/ import commands import datetime import feedparser import MySQLdb import os import re import shutil import sys import time import unicodedata import urllib from socket import gethostname from stat import * __author__ = 'Michael Still (mikal@google.com)' __version__ = 'Release 1' ## # IpTvVideo ## class IpTvVideo: """IpTvVideo -- video handling methods This uses mplayer to determine the length of the video. Specifically, this command line does the trick: mplayer -frames 0 -identify 2>&1 | grep ID_LENGTH """ def __init__(self, db, filename): """__init__ -- prime the pump to answer questions about the video""" self.db = db self.filename = filename self.values = {} out = commands.getoutput('mplayer -frames 0 -identify %s 2>&1 | grep "="' \ % self.filename) for line in out.split('\n'): try: (key, value) = line.split('=') self.values[key] = value except: dummy = 'blah' def Length(self): """Length -- return the length of the video in seconds""" return float(self.values['ID_LENGTH']) def NeedsTranscode(self): """NeedsTranscode -- decide if a video needs transcoding before import""" # Doesn't need transcoding return_false = ['0x10000002', 'divx', 'XVID'] # Does need transcoding return_true = ['avc1', 'mp4v', 'theo', 'WMV2'] if self.values['ID_VIDEO_FORMAT'] in return_false: return False if self.values['ID_VIDEO_FORMAT'] in return_true: return True else: print print '****************************************************************' print 'I don\'t know if we need to transcode videos in %s format' \ % self.values['ID_VIDEO_FORMAT'] print 'I\'m going to give it a go without, and it if doesn\'t work' print 'please report it to mikal@stillhq.com' print '****************************************************************' print return False def NewFilename(self, extn): """NewFilename -- determine what filename to use after transcoding""" # We only return the filename portion, not the path re_filename = re.compile('^(.+)/(.+?)$') m = re_filename.match(self.filename) if m: file = m.group(2) else: file = self.filename # Try changing the extension to extn re_extension = re.compile('(.*)\.(.*?)') m = re_extension.match(file) if m: return '%s.%s' % (m.group(1), extn) return 'new-%s' % file def Transcode(self, datadir): """Transcode -- transcode the video to a better format. Returns the new filename. """ # If the file is small, go for a format which will hopefully look nicer print 'Transcoding' format = '-ovc lavc -oac lavc -ffourcc DX50' newfilename = self.NewFilename('avi') start_size = os.stat(self.filename)[ST_SIZE] # This currently results in videos which crash the frontend. # if start_size < 1024 * 1024 * 100: # db.Log('File is small, using nuv for transcode') # format = '-ovc nuv -oac mp3lame' # newfilename = self.NewFilename('nuv') command = 'mencoder %s %s -o %s/%s' %(self.filename, format, datadir, newfilename) (status, out) = commands.getstatusoutput(command) if status != 0: db.Log('Transcode failed: %s' % status) print 'Transcode failed: %s' % status print out print print 'The command line was: %s' % command sys.exit(1) # Log the file growth db.Log('Transcoding changed size of file from %d to %d' \ %(start_size, os.stat('%s/%s' %(datadir, newfilename))[ST_SIZE])) return newfilename ## # IpTvDatabase ## class IpTvDatabase: """IpTvDatavase -- handle all MySQL details""" def __init__(self): self.OpenConnection() self.CheckSchema() self.CleanLog() self.RepairMissingDates() def OpenConnection(self): """OpenConnection -- parse the MythTV config file and open a connection to the MySQL database""" # Load the text configuration file self.config_values = {} home = os.environ.get('HOME') try: config = open(home + '/.mythtv/mysql.txt') for line in config.readlines(): if not line.startswith('#') and len(line) > 5: (key, value) = line.rstrip('\n').split('=') self.config_values[key] = value except: print 'Could not parse the MySQL configuration for MythTV from', print '~/.mythtv/mysql.txt' sys.exit(1) # Open the DB connection try: self.db_connection = MySQLdb.connect( host = self.config_values['DBHostName'], user = self.config_values['DBUserName'], passwd = self.config_values['DBPassword'], db = self.config_values['DBName']) except: print 'Could not connect to the MySQL server defined in ', print '~/.mythtv/mysql.txt' sys.exit(1) def TableExists(self, table): """TableExists -- check if a table exists""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) try: cursor.execute('describe %s;' % table) except MySQLdb.Error, (errno, errstr): if errno == 1146: return False else: print 'Error %d: %s' %(errno, errstr) sys.exit(1) cursor.close() return True def CheckSchema(self): """CheckSchema -- ensure we're running the latest schema version""" # Check if we even have an NetTv set of tables for table in ['log', 'settings', 'programs', 'subscriptions']: if not self.TableExists('mythnettv_%s' % table): if self.TableExists('mythiptv_%s' % table): self.Log('Renaming table %s to %s;' \ %('mythiptv_%s' % table, 'mythnettv_%s' % table)) self.ExecuteSql('rename table %s to %s;' \ %('mythiptv_%s' % table, 'mythnettv_%s' % table)) else: self.CreateTable(table) # Check the schema version self.version = self.GetSetting('schema') if self.version != '7': self.UpdateTables() # Make sure we have a chanid chanid = self.GetSetting('chanid') if chanid == None: channels_row = None try: # There is none cached in the settings table channels_row = self.GetOneRow('select chanid from channel where ' 'name = "MythNetTV" or ' 'name = "MythIPTV";') except: print 'There was a MySQL error when trying to read the channels ', print 'this probably indicates an error with your MySQL installation' sys.exit(1) if channels_row: # There is one in the MythTV Channels table though chanid = self.GetSettingWithDefault('chanid', channels_row['chanid']) else: # There isn't one in the MythTV Channels table chanid_row = self.GetOneRow('select max(chanid) + 1 from channel') if chanid_row.has_key('max(chanid) + 1'): chanid = chanid_row['max(chanid) + 1'] else: chanid = 1 self.db_connection.query('insert into channel (chanid, callsign, ' 'name, commfree) values (%d, "MythNetTV", ' '"MythNetTV", 1)' % chanid) self.Log('Created MythNetTV channel with chanid %d' % chanid) # Redo the selecting to make sure it worked channels_row = self.GetOneRow('select chanid from channel where ' 'name = "MythNetTV";') chanid = self.GetSettingWithDefault('chanid', channels_row['chanid']) # Make sure that we're using the new name for the channel, and that we # use an @ to make it display properly in the UI self.db_connection.query('update channel set callsign = "MythNetTV", ' 'name = "MythNetTV" where name = "MythIPTV";') self.db_connection.query('update channel set channum = "@" ' 'where name = "MythNetTV" and channum is null;') def RepairMissingDates(self): """RepairMissingDates -- repair programs which are missing a date""" # At some point there was a bug which resulted in there being programs # in the MythNetTV TODO list which didn't have dates associated with them. # This doesn't have any nasty side effects, but will result in incorrect # ordering in the MythTV recordings interface, and downloads happening # out of order. We try to clean the problem up here, and report to the # the user if we need to. touched_count = 0 # Try using parsed date for row in self.GetRows('select guid, parsed_date, unparsed_date from ' 'mythnettv_programs where date is null and ' 'parsed_date like "(%)";'): if row.has_key('parsed_date') and row['parsed_date'] != None: parsed = row['parsed_date'][1:-1].split(', ') parsed_ints = [] for item in parsed: parsed_ints.append(int(item)) date = datetime.datetime(*parsed_ints[0:5]) program = IpTvProgram(self) program.Load(row['guid']) program.SetDate(date) program.Store() touched_count += 1 # Otherwise, just set it to now and get on with our lives for row in self.GetRows('select guid from mythnettv_programs ' 'where date is null;'): program = IpTvProgram(self) program.Load(row['guid']) program.SetDate(datetime.datetime.now()) program.Store() touched_count += 1 if touched_count > 0: print 'During startup, I found %d programs with invalid dates. This' \ % touched_count print 'indicates a bug in MythNetTV. I think its corrected now. If' print 'this message keeps appearing, please email mikal@stillhq.com' print 'and let him know.' def GetSetting(self, name): """GetSetting -- get the current value of a setting""" row = self.GetOneRow('select value from mythnettv_settings where ' 'name="%s";' % name) if row == None: return None return row['value'] def GetSettingWithDefault(self, name, default): """GetSettingWithDefault -- get a setting with a default value""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('select value from mythnettv_settings where ' 'name="%s";' % name) if cursor.rowcount != 0: retval = cursor.fetchone() cursor.close() return retval['value'] else: self.db_connection.query('insert into mythnettv_settings (name, value) ' 'values("%s", "%s");' %(name, default)) self.Log('Settings value %s defaulted to %s' %(name, default)) return default def GetOneRow(self, sql): """GetOneRow -- get one row which matches a query""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute(sql) retval = cursor.fetchone() cursor.close() if retval == None: return retval for key in retval.keys(): if retval[key] == None: del retval[key] return retval def GetRows(self, sql): """GetRows -- return a bunch of rows as an array of dictionaries""" retval = [] cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute(sql) for i in range(cursor.rowcount): row = cursor.fetchone() retval.append(row) return retval def GetWaitingForImport(self): """GetWaitingForImport -- return a list of the guids waiting for import""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('select guid from mythnettv_programs where ' 'download_finished = "1" and imported is NULL') guids = [] while True: program = cursor.fetchone() if program == None: break guids.append(program['guid']) return guids def FormatSqlValue(self, name, value): """FormatSqlValue -- some values get escaped for SQL use""" if type(value) == datetime.datetime: return 'STR_TO_DATE("%s", "%s")' \ %(value.strftime('%a, %d %b %Y %H:%M:%S'), '''%a, %d %b %Y %H:%i:%s''') if name == 'date': return 'STR_TO_DATE("%s", "%s")' %(value, '''%a, %d %b %Y %H:%i:%s''') if type(value) == long or type(value) == int: return value return '"%s"' % value.replace('"', '""') def WriteOneRow(self, table, key_col, dict): """WriteOneRow -- use a dictionary to write a row to the specified table""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('select %s from %s where %s = "%s"' \ %(key_col, table, key_col, dict[key_col])) if cursor.rowcount > 0: self.Log('Updating %s row with %s of %s' %(table, key_col, dict[key_col])) vals = [] for col in dict: val = '%s=%s' %(col, self.FormatSqlValue(col, dict[col])) vals.append(val) sql = 'update %s set %s where %s="%s";' %(table, ','.join(vals), key_col, dict[key_col]) else: self.Log('Creating %s row with %s of %s' %(table, key_col, dict[key_col])) vals = [] for col in dict: val = self.FormatSqlValue(col, dict[col]) vals.append(val) sql = 'insert into %s (%s) values(%s);' \ %(table, ','.join(dict.keys()), ','.join(vals)) cursor.close() self.db_connection.query(sql) def GetNextLogSequenceNumber(self): """GetNextLogSequenceNumber -- ghetto lookup of the highest sequence number""" try: cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('select max(sequence) + 1 from mythnettv_log;') retval = cursor.fetchone() cursor.close() if retval['max(sequence) + 1'] == None: return 1 return retval['max(sequence) + 1'] except: return 1 def Log(self, message): """Log -- write a log message to the database""" try: new_sequence = self.GetNextLogSequenceNumber() self.db_connection.query('insert into mythnettv_log (sequence, ' 'timestamp, message) values(%d, NOW(), "%s");' \ %(new_sequence, message)) except: print 'Failed to log: %s' % message def CleanLog(self): """CleanLog -- remove all but the newest xxx log messages""" min_sequence = self.GetNextLogSequenceNumber() - \ int(self.GetSettingWithDefault('loglines', '1000')) - 1 cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('delete from mythnettv_log where sequence < %d' \ % min_sequence) if cursor.rowcount > 0: self.Log('Deleted %d log lines before sequence number %d' \ %(cursor.rowcount, min_sequence)) cursor.close() def CreateTable(self, tablename): """CreateTable -- a table has been found to be missing, create it with the current schema""" print 'Info: Creating %s table' % tablename if tablename == 'log': self.db_connection.query('create table mythnettv_log (sequence int, ' 'timestamp datetime, message text);') self.db_connection.query('insert into mythnettv_log (sequence) ' 'values(0);') elif tablename == 'settings': self.db_connection.query('create table mythnettv_settings (name text, ' 'value text);') self.db_connection.query('insert into mythnettv_settings (name, value) ' 'values("schema", 7);') elif tablename == 'programs': self.db_connection.query('create table mythnettv_programs (guid text, ' 'url text, title text, subtitle text, ' 'description text unicode, date datetime, ' 'unparsed_date text, parsed_date text, ' 'download_started int, ' 'download_finished int, ' 'imported int, transfered int, size int, ' 'filename text);') elif tablename == 'subscriptions': self.db_connection.query('create table mythnettv_subscriptions (' 'url text, title text);') else: self.Log('Error: Don\'t know how to create %s' % tablename) print 'Error: Don\'t know how to create %s' % tablename sys.exit(1) self.Log('Creating %s table' % tablename) def UpdateTables(self): """UpdateTables -- handle schema upgrades""" if self.version == '4': self.Log('Upgrading schema from 4 to 5') self.db_connection.query('alter table mythnettv_programs ' 'add parsed_date text;') self.version = '5' if self.version == '5': # This is a deliberate noop because the new table was created during the # startup checks self.Log('Upgrading schema from 5 to 6') self.version = '6' if self.version == '6': # Another noop, because we're renaming tables self.Log('Upgrading schema from 6 to 7') self.version = '7' self.db_connection.query('update mythnettv_settings set value = "%s" where ' 'name = "schema";' % self.version) def ExecuteSql(self, sql): self.db_connection.query(sql) ## # IpTvProgram ## class IpTvProgram: """IpTvProgram -- a downloadable program. This class embodies everything we can do with a program. The existance of this class does not mean that the show has been fully downloaded and made available in MythTV yet. Instances of this class persist to the MySQL database. """ def __init__(self, db): self.persistant = {} self.db = db def FromUrl(self, url, guid): """FromUrl -- start a program based on its URL""" new_video = True # Some URLs have the ampersand escaped url = url.replace('&', '&') # Persist what we know now self.persistant['url'] = url self.persistant['filename'] = self.GetFilename(url) self.persistant['guid'] = guid try: if self.db.GetOneRow('select * from mythnettv_programs ' 'where guid="%s";' % guid).keys() != []: new_video = False except: dummy = 'blah' self.Store() self.db.Log('Updated show from %s with guid %s' %(url, guid)) return new_video def FromInteractive(self, url, title, subtitle, description): """FromInteractive -- create a program by prompting the user for input for all the bits we need. We check if we have the data first, so that we're not too annoying. """ if url: self.persistant['url'] = url if title: self.persistant['title'] = title if subtitle: self.persistant['subtitle'] = subtitle if description: self.persistant['description'] = description for key in ['url', 'title', 'subtitle', 'description']: if not self.persistant.has_key(key): self.persistant[key] = Prompt(key) # TODO(mikal): Should I generate a more unique GUID? self.persistant['guid'] = self.persistant['url'] self.persistant['filename'] = self.GetFilename(self.persistant['url']) self.Store() def GetFilename(self, url): """GetFilename -- return the filename portion of a URL""" # Some URLs have the ampersand escaped re_filename = re.compile('.*/([^/\?]*).*') m = re_filename.match(url) if m: return m.group(1) if not '/' in url: return url print 'Could not determine local filename for %s' % url sys.exit(1) def GetTitle(self): """GetTitle -- return the title of the program""" return self.persistant['title'] def GetSubtitle(self): """GetSubtitle -- return the subtitle of the program""" return self.persistant['subtitle'] def GetDate(self): """GetDate -- return the date of the program""" return self.persistant['unparsed_date'] def SetDate(self, date): """SetDate -- set the date of the program""" self.persistant['date'] = date.strftime('%a, %d %b %Y %H:%M:%S') self.persistant['unparsed_date'] = date.strftime('%a, %d %b %Y %H:%M:%S') self.persistant['parsed_date'] = date def Load(self, guid): """Load -- load information based on a GUID from the DB""" self.persistant = self.db.GetOneRow('select * from mythnettv_programs ' 'where guid="%s";' % guid) def Store(self): """Store -- persist to MySQL""" # We store the date of the entry a lot of different ways if not self.persistant.has_key('date'): self.SetDate(datetime.datetime.now()) try: self.db.WriteOneRow('mythnettv_programs', 'guid', self.persistant) except MySQLdb.Error, (errno, errstr): if errno != 1064: self.db.Log('Could not store program: %s (%d, %s)' \ %(self.persistant['guid'], errno, errstr)) print 'Could not store program %s' % self.persistant['guid'] except Exception: self.db.Log('Could not store program: %s' \ %(self.persistant['guid'])) print 'Could not store program %s' % self.persistant['guid'] def SetUrl(self, url): """SetUrl -- set just the URL for the program""" self.persistant['url'] = url def SetShowInfo(self, title, subtitle, description, date, date_parsed): """SetShowInfo -- set show meta data""" self.persistant['title'] = title self.persistant['subtitle'] = subtitle self.persistant['description'] = \ unicodedata.normalize('NFKD', description).encode('ascii', 'ignore') self.persistant['date'] = date self.persistant['unparsed_date'] = date self.persistant['parsed_date'] = repr(date_parsed) self.Store() self.db.Log('Set show info for guid %s' % self.persistant['guid']) def TemporaryFilename(self, datadir): """TemporaryFilename -- calculate the filename to use in the temporary directory """ filename = '%s/%s' %(datadir, self.persistant['filename']) print 'Destination will be %s' % filename self.db.Log('Downloading %s to %s' %(self.persistant['guid'], filename)) return filename def DownloadMPlayer(self, filename): """DownloadRTSP -- download a show using mplayer""" datadir = db.GetSettingWithDefault('datadir', 'data') (status, out) = commands.getstatusoutput('cd %s; mplayer -dumpstream "%s"' % (datadir, self.persistant['url'])) if status != 0: db.Log('MPlayer download failed') print 'MPlayer download failed' return 0 shutil.move(datadir + '/stream.dump', filename) return os.stat(filename)[ST_SIZE] def DownloadHTTP(self, filename): """DownloadHTTP -- download a show, using HTTP""" done = self.persistant.get('download_finished', '0') if done != '1': print 'Downloading' remote = urllib.urlopen(self.persistant['url']) local = open(filename, 'w') total = int(self.persistant.get('transfered', 0)) count = 0 while done != '1': data = remote.read(1024) length = len(data) if length < 1024: done = '1' local.write(data) total += len(data) if count > 30000: self.persistant['transfered'] = repr(total) # TODO(mikal): Size should be determined beforehand if possible self.persistant['size'] = repr(total) self.Store() count = 0 count += 1 print 'Finished' remote.close() local.close() return total def Download(self, datadir): """Download -- download the show""" filename = self.TemporaryFilename(datadir) self.persistant['download_started'] = '1' self.Store() total = 0 if self.persistant['url'].startswith('http://'): total = self.DownloadHTTP(filename) else: total = self.DownloadMPlayer(filename) # else: # self.db.Log('Protocol for %s undefined' % self.persistant['url']) # print 'Protocol for %s undefined' % self.persistant['url'] # return False if total == 0: return False self.persistant['download_finished'] = '1' self.persistant['transfered'] = repr(total) self.persistant['size'] = repr(total) self.Store() print 'Done' self.db.Log('Download of %s done' % self.persistant['guid']) return True def CopyLocalFile(self, datadir): """CopyLocalFile -- copy a local file to the temporary directory, and treat it as if it was a download""" filename = self.TemporaryFilename(datadir) self.persistant['download_started'] = '1' self.Store() if self.persistant['url'] != filename: shutil.copyfile(self.persistant['url'], filename) self.persistant['download_finished'] = '1' size = os.stat(filename)[ST_SIZE] self.persistant['transfered'] = repr(size) self.persistant['size'] = repr(size) self.Store() print 'Done' self.db.Log('Download of %s done' % self.persistant['guid']) def Import(self): """Import -- import a downloaded show into the MythTV user interface""" # Determine meta data self.db.Log('Importing %s' % self.persistant['guid']) datadir = db.GetSettingWithDefault('datadir', 'data') chanid = self.db.GetSetting('chanid') filename = '%s/%s' %(datadir, self.persistant['filename']) videodir = self.db.GetOneRow('select * from settings where value = ' '"RecordFilePrefix" and hostname = "%s";' \ % gethostname())['data'] video = IpTvVideo(db, filename) # Try to use the publish time of the RSS entry as the start time... # The tuple will be in the format: 2003, 8, 6, 20, 43, 20 try: tuple = eval(self.persistant['parsed_date']) start = datetime.datetime(tuple[0], tuple[1], tuple[2], tuple[3], tuple[4], tuple[5]) except: start = datetime.datetime.now() # Ensure uniqueness for the start time interval = datetime.timedelta(seconds = 1) while not self.db.GetOneRow('select basename from recorded where ' 'starttime = %s and chanid = %s and ' 'basename != "%s"' \ %(self.db.FormatSqlValue('', start), chanid, filename)) == None: start += interval # Determine the duration of the video duration = datetime.timedelta(seconds = video.Length()) finish = start + duration # Transcode file to a better format if needed. transcoded is the filename # without the data directory portion if video.NeedsTranscode(): transcoded = video.Transcode(datadir) os.remove(filename) else: re_justfilename = re.compile('^(.*)/(.+?)$') m = re_justfilename.match(filename) if m: transcoded = m.group(2) else: transcoded = filename # Make sure we haven't loaded this file before if not self.db.GetOneRow('select * from recorded where basename = "%s"' \ % filename) == None: print 'Already imported %s (%s)' %(self.persistant['guid'], filename) else: print 'Importing video %s...' % self.persistant['guid'] shutil.move(datadir + '/' + transcoded, videodir + '/' + transcoded) print 'Creating row' self.db.ExecuteSql('insert into recorded (chanid, starttime, endtime, ' \ 'title, subtitle, description, hostname, basename, ' \ 'progstart, progend) values (%s, %s, %s, "%s", ' \ '"%s", %s, "%s", "%s", %s, %s)' \ %(chanid, self.db.FormatSqlValue('', start), self.db.FormatSqlValue('', finish), self.persistant['title'], self.persistant['subtitle'], self.db.FormatSqlValue('', self.persistant['description']), gethostname(), transcoded, self.db.FormatSqlValue('', start), self.db.FormatSqlValue('', finish))) print 'Rebuilding seek table' commands.getoutput('mythcommflag --rebuild --file "%s"' \ % videodir + '/' + transcoded) print 'Adding commercial flag job to backend queue' commands.getoutput('mythcommflag --queue --file "%s"' \ % videodir + '/' + transcoded) self.SetImported() print 'Done' print # And now mark the video as imported return def SetImported(self): """SetImported -- flag this program as having been imported""" self.persistant['download_finished'] = 1 self.persistant['imported'] = '1' self.Store() ## # Syncing helpers ## re_attributeparser = re.compile('([^=]*)="([^"]*)" *(.*)') def ParseAttributes(inputline): """ParseAttributes -- used to unmangle XML entity attributes""" line = inputline result = {} m = re_attributeparser.match(line) while m: result[m.group(1)] = m.group(2) line = m.group(3) m = re_attributeparser.match(line) return result def Download(db, url, guid, title, subtitle, description, date, date_parsed): """Download -- add a program to the list of waiting downloads""" program = IpTvProgram(db) if program.FromUrl(url, guid): print 'Creating program for %s: %s from %s' %(title, subtitle, guid) print program.SetShowInfo(title, subtitle, description, date, date_parsed) def Sync(db, xmlfile, title): """Sync -- sync up with an RSS feed""" # Grab the XML xmllines = xmlfile.readlines() # Modify the XML to work around namespace handling bugs in FeedParser lines = [] re_mediacontent = re.compile('(.*)]*)/ *>(.*)') for line in xmllines: m = re_mediacontent.match(line) count = 1 while m: line = '%s%s%s' %(m.group(1), count, m.group(2), count, m.group(3)) m = re_mediacontent.match(line) count = count + 1 lines.append(line) # Parse the modified XML xml = ''.join(lines) parser = feedparser.parse(xml) # Find the media:content entries for entry in parser.entries: videos = {} description = entry.description subtitle = entry.title if entry.has_key('media_description'): description = entry['media_description'] # Enclosures if entry.has_key('enclosures'): for enclosure in entry.enclosures: videos[enclosure.type] = enclosure # Media:RSS for key in entry.keys(): if key.startswith('media_wannabe'): attrs = ParseAttributes(entry[key]) if attrs.has_key('type'): videos[attrs['type']] = attrs if attrs.has_key('title'): subtitle = attrs['title'] if videos.has_key('video/x-msvideo'): Download(db, videos['video/x-msvideo']['url'], entry.guid, title, subtitle, description, entry.date, entry.date_parsed) elif videos.has_key('video/mp4'): Download(db, videos['video/mp4']['url'], entry.guid, title, subtitle, description, entry.date, entry.date_parsed) elif videos.has_key('video/x-xvid'): Download(db, videos['video/x-xvid']['url'], entry.guid, title, subtitle, description, entry.date, entry.date_parsed) elif videos.has_key('video/quicktime'): Download(db, videos['video/quicktime']['url'], entry.guid, title, subtitle, description, entry.date, entry.date_parsed) elif videos.has_key('text/html'): db.Log('Warning: Treating text/html as an video enclosure type for ' '%s' % entry.guid) print 'Warning: Treating text/html as an video enclosure from %s for ' \ '%s pointing to %s ' %(repr(videos.keys()), subtitle, videos['text/html']['url']) Download(db, videos['text/html']['url'], entry.guid, title, subtitle, description, entry.date, entry.date_parsed) else: print 'Error: Unsure which to prefer from: %s for %s' \ %(repr(videos.keys()), subtitle) def NextDownloads(count, filter, only_old): """NextDownloads -- return a list of the GUIDs to download next. Optionally filter based on an exact match of title string. """ remaining = int(count) old_target = remaining / 2 if only_old: old_target = remaining guids = [] if filter == None: title = 'is not NULL' else: title = '= "%s"' % filter print 'Download constrained to "%s"' % filter for row in db.GetRows('select guid from mythnettv_programs where ' 'download_finished is NULL and title %s ' 'order by date asc limit %d;' \ %(title, old_target)): guids.append(row['guid']) remaining -= 1 if only_old: return guids for row in db.GetRows('select guid from mythnettv_programs where ' 'download_finished is NULL and title %s ' 'order by date desc limit %d;' %(title, remaining)): guids.append(row['guid']) return guids def DownloadAndImport(db, guid): """DownloadAndImport -- perform all the steps to download and import a given guid. """ print 'Downloading %s' % guid program = IpTvProgram(db) program.Load(guid) if program.Download(db.GetSettingWithDefault('datadir', 'data')) == True: program.Import() def Prompt(prompt): """Prompt -- prompt for input from the user""" sys.stdout.write('%s >> ' % prompt) return sys.stdin.readline().rstrip('\n') def DisplayFriendlySize(bytes): """DisplayFriendlySize -- turn a number of bytes into a nice string""" if bytes < 1024: return '%d bytes' % bytes if bytes < 1024 * 1024: return '%d kb (%d bytes)' %((bytes / 1024), bytes) if bytes < 1024 * 1024 * 1024: return '%d mb (%d bytes)' %((bytes / (1024 * 1024)), bytes) return '%d gb (%d bytes)' %((bytes / (1024 * 1024 * 1024)), bytes) def GetPossible(array, index): """GetPossible -- get a value from an array, handling its absence nicely""" try: return array[index] except: return None def Usage(): print 'Unknown command line. Try one of:' print print '(manual usage)' print ' --url : to download an RSS feed and load the shows' print ' from it into the TODO list. The title is' print ' as the show title in the MythTV user' print ' interface' print ' --file <url> <title>: to do the same, but from a file, with a show' print ' title like --url above' print ' --download <num> : to download that number of shows from the' print ' TODO list. We download some of the oldest' print ' first, and then grab some of the newest as' print ' well.' print ' --download <num> <title filter>' print ' : the same as above, but filter to only download' print ' shows with a title exactly matching the' print ' specified filter' print ' --cleartodo : permanently remove all items from the TODO' print ' list' print ' --markread <num> : interactively mark some of the oldest <num>' print ' shows as already downloaded and imported' print print '(handy stuff)' print ' --todoremote : add a remote URL to the TODO list. This will' print ' prompt for needed information about the' print ' video, and set the date of the program to' print ' now' print ' --todoremote <url> <title> <subtitle> <description>' print ' : the same as above, but don\'t prompt for' print ' anything' print ' --importremote : download and immediately import the named' print ' URL. Will prompt for needed information' print ' --importremote <url> <title> <subtitle> <description>' print ' : the same as above, but don\'t prompt for' print ' anything' print ' --importlocal <file>: import the named file. The file will be' print ' left on disk. Will prompt for needed' print ' information' print print '(subscription management)' print ' --subscribe <url> <title>' print ' : subscribe to a URL, and specify the show title' print ' --list : list subscriptions' print ' --unsubscribe <url> : unsubscribe from a URL' print ' --update : add new programs from subscribed URLs to the' print ' TODO list' print print '(reporting)' print ' --statistics : show some simple statistics about MythNetTV' print ' --log : dump the current internal log entries' print ' --nextdownload <num>' print ' : if you executed --download <num>, what would' print ' be downloaded?' print sys.exit(1) if __name__ == "__main__": db = IpTvDatabase() # TODO(mikal): This command line processing is ghetto if len(sys.argv) == 1: Usage() if sys.argv[1] == '--url': # Go and grab the XML file from the remote HTTP server, and then parse it # as an RSS feed with enclosures. Populates a TODO list in # mythnettv_programs xmlfile = urllib.urlopen(sys.argv[2]) Sync(db, xmlfile, sys.argv[3]) elif sys.argv[1] == '--file': # Treat the local file as an RSS feed. Populates a TODO list in # mythnettv_programs xmlfile = open(sys.argv[2]) Sync(db, xmlfile, sys.argv[3]) elif sys.argv[1] == '--download': # Download the specified number of programs and import them into MythTV filter = GetPossible(sys.argv, 3) for guid in NextDownloads(sys.argv[2], filter, False): DownloadAndImport(db, guid) # And now make sure there aren't any stragglers for guid in db.GetWaitingForImport(): program = IpTvProgram(db) program.Load(guid) try: program.Import() except: print 'Couldn\'t import straggling %s, removing it from the queue' % guid program.SetImported() elif sys.argv[1] == '--todoremote': # Add a remote URL to the TODO list. We have to prompt for a bunch of stuff # because we don't have a "real" RSS feed program = IpTvProgram(db) url = GetPossible(sys.argv, 2) title = GetPossible(sys.argv, 3) subtitle = GetPossible(sys.argv, 4) description = GetPossible(sys.argv, 5) program.FromInteractive(url, title, subtitle, description) elif sys.argv[1] == '--importremote': # Download a remote file and then import it as a program. We have to prompt # for details here, because this didn't come from a "real" RSS feed program = IpTvProgram(db) url = GetPossible(sys.argv, 2) title = GetPossible(sys.argv, 3) subtitle = GetPossible(sys.argv, 4) description = GetPossible(sys.argv, 5) program.FromInteractive(url, title, subtitle, description) if program.Download(db.GetSettingWithDefault('datadir', 'data')) == True: program.Import() elif sys.argv[1] == '--importlocal': # Take a local file, copy it to the temporary directory, and then import # it as if we had downloaded it program = IpTvProgram(db) program.SetUrl(sys.argv[2]) url = GetPossible(sys.argv, 2) title = GetPossible(sys.argv, 3) subtitle = GetPossible(sys.argv, 4) description = GetPossible(sys.argv, 5) program.FromInteractive(url, title, subtitle, description) program.CopyLocalFile(db.GetSettingWithDefault('datadir', 'data')) program.Import() elif sys.argv[1] == '--subscribe': # Subscribe to an RSS feed db.WriteOneRow('mythnettv_subscriptions', 'url', {'url':sys.argv[2], 'title':sys.argv[3]}) elif sys.argv[1] == '--list': # List subscribed RSS feeds for row in db.GetRows('select * from mythnettv_subscriptions'): print '%s: %s' %(row['title'], row['url']) elif sys.argv[1] == '--unsubscribe': # Remove a subscription to an RSS feed db.ExecuteSql('delete from mythnettv_subscriptions where url = "%s";' \ % sys.argv[2]) elif sys.argv[1] == '--update': # Update the TODO list based on subscriptions for row in db.GetRows('select * from mythnettv_subscriptions'): xmlfile = urllib.urlopen(row['url']) Sync(db, xmlfile, row['title']) elif sys.argv[1] == '--statistics': # Display some simple stats about the state of MythIPTV row = db.GetOneRow('select count(guid) from mythnettv_programs;') print 'Programs tracked: %d' % row['count(guid)'] for show in db.GetRows('select distinct(title) from mythnettv_programs ' 'where title is not NULL;'): row = db.GetOneRow('select count(guid) from mythnettv_programs where ' 'title = "%s"' % show['title']) print ' %s: %d' %(show['title'], row['count(guid)']) print row = db.GetOneRow('select count(guid) from mythnettv_programs where ' 'download_finished is NULL and title is not NULL;') print 'Programs still to download: %d' % row['count(guid)'] for show in db.GetRows('select distinct(title) from mythnettv_programs ' 'where title is not NULL and ' 'download_finished is NULL;'): row = db.GetOneRow('select count(guid) from mythnettv_programs where ' 'title = "%s" and download_finished is NULL' \ % show['title']) print ' %s: %d' %(show['title'], row['count(guid)']) print try: row = db.GetOneRow('select sum(transfered) from mythnettv_programs;') print 'Data transferred: %s' \ % (DisplayFriendlySize(int(row['sum(transfered)']))) except: # TODO(mikal): I am sure there is a better way of doing this dummy = 'blah' elif sys.argv[1] == '--log': for logline in db.GetRows('select * from mythnettv_log order by ' 'sequence asc;'): print '%s %s' %(logline['timestamp'], logline['message']) elif sys.argv[1] == '--nextdownload': for guid in NextDownloads(sys.argv[2], None, False): print guid program = IpTvProgram(db) program.Load(guid) print ' %s: %s' %(program.GetTitle(), program.GetSubtitle()) print elif sys.argv[1] == '--cleartodo': print 'The command you are executing will permanently remove all shows' print 'from the TODO list, as well as any record of shows which have' print 'already been downloaded. Basically you\'ll be back at the start' print 'again, although your preferences will remain set. Are you sure' print 'you want to do this?' print confirm = raw_input('Type yes to do this: ') if confirm == 'yes': db.ExecuteSql('delete from mythnettv_programs;') print 'Deleted' elif sys.argv[1] == '--markread': for guid in NextDownloads(sys.argv[2], None, True): program = IpTvProgram(db) program.Load(guid) print ' %s: %s' %(program.GetTitle(), program.GetSubtitle()) print ' (%s)' % program.GetDate() print print 'Are you sure you want to mark this show as downloaded?' print confirm = raw_input('Type yes to do this: ') if confirm == 'yes': program.SetImported() print 'Done' print else: Usage()