#!/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 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 * ## # 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, filename): """__init__ -- prime the pump to answer questions about the video""" 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""" if self.values['ID_VIDEO_FORMAT'] == 'avc1': return True if self.values['ID_VIDEO_FORMAT'] == 'divx': return False if self.values['ID_VIDEO_FORMAT'] == 'theo': 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): """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 'avi' re_extension = re.compile('(.*)\.(.*?)') m = re_extension.match(file) if m: return '%s.avi' % m.group(1) return 'new-%s' % file def Transcode(self, datadir): """Transcode -- transcode the video to a better format. Returns the new filename. """ print 'Transcoding' newfilename = self.NewFilename() command = 'mencoder %s -ovc lavc -oac lavc -ffourcc DX50 ' \ '-o %s/%s' %(self.filename, datadir, newfilename) (status, out) = commands.getstatusoutput(command) if status != 0: print 'Transcode failed: %s' % status print out print print 'The command line was: %s' % command sys.exit(1) return newfilename ## # IpTvDatabase ## class IpTvDatabase: """IpTvDatavase -- handle all MySQL details""" def __init__(self): self.OpenConnection() self.CheckSchema() self.CleanLog() 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') 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 # Open the DB connection 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']) 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 # TODO(mikal): Hmmm, I need to work out why the channel doesn't display # properly in the UI chanid = self.GetSetting('chanid') if chanid == None: # There is none cached in the settings table channels_row = self.GetOneRow('select chanid from channel where ' 'name = "MythIPTV";') if channels_row != None: # 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 = self.GetOneRow('select max(chanid) + 1 from channel') \ ['max(chanid) + 1'] self.db_connection.query('insert into channel (chanid, callsign, name, ' 'commfree) values (%d, "MythIPTV", ' '"MythIPTV", 1)' % chanid) self.Log('Created IPTV channel with chanid %d' % chanid) # Redo the selecting to make sure it worked channels_row = self.GetOneRow('select chanid from channel where ' 'name = "MythIPTV";') chanid = self.GetSettingWithDefault('chanid', channels_row['chanid']) 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 name == 'date': return 'STR_TO_DATE("%s", "%s")' %(value, '''%a, %d %b %Y %H:%i:%s''') 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 type(value) == long: 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""" cursor = self.db_connection.cursor(MySQLdb.cursors.DictCursor) cursor.execute('select max(sequence) + 1 from mythnettv_log;') retval = cursor.fetchone() cursor.close() return retval['max(sequence) + 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 it's URL""" # 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 self.Store() self.db.Log('Created show from %s with guid %s' %(url, guid)) 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 != None: self.persistant['url'] = url if title != None: self.persistant['title'] = title if subtitle != None: self.persistant['subtitle'] = subtitle if description != None: 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']) # We store the date of the entry a lot of different ways if not self.persistant.has_key('date'): now = datetime.datetime.now() self.persistant['date'] = now.strftime('%a, %d %b %Y %H:%M:%S') self.persistant['unparsed_date'] = now.strftime('%a, %d %b %Y %H:%M:%S') self.persistant['parsed_date'] = now 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 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""" 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)) 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']) print 'Show date is %s' % date 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 Download(self, datadir): """Download -- download the show""" filename = self.TemporaryFilename(datadir) self.persistant['download_started'] = '1' self.Store() done = self.persistant.get('download_finished', '0') if done != '1': print 'Downloading' remote = urllib.urlopen(self.persistant['url']) local = open(filename, 'w') total = 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() 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 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(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 'Flagging commericals' # TODO(mikal): Perhaps this should just create a job in the queue instead commands.getoutput('mythcommflag --file "%s"' \ % videodir + '/' + transcoded) self.persistant['imported'] = '1'; self.Store() print 'Done' print # And now mark the video as imported return ## # 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""" print 'Creating program for %s: %s from %s' %(title, subtitle, guid) program = IpTvProgram(db) program.FromUrl(url, guid) 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 print 'Apply media:content work around' 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) if parser.feed.has_key('title'): print parser.feed.title elif parser.feed.has_key('description'): print parser.feed.description else: print '\tUrl: %s'%(url) print '' # Find the media:content entries print 'Info: Processing feed %s (%d entries)' %(title, len(parser.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) else: print 'Error: Unsure which to prefer from: %s for %s' \ %(repr(videos.keys()), subtitle) def NextDownloads(count): """NextDownloads -- return a list of the GUIDs to download next""" # The algorithm here changed for beta 2. I now make the assumption that # people are impatient, and that a download run should involve some # combination of the oldest queued programs, and some of the newest. remaining = int(count) guids = {} for row in db.GetRows('select guid from mythnettv_programs where ' 'download_finished is NULL and title is not NULL ' 'order by date asc limit %d;' % (remaining / 2)): guids[row['guid']] = 'yes' remaining -= 1 for row in db.GetRows('select guid from mythnettv_programs where ' 'download_finished is NULL and title is not NULL ' 'order by date desc limit %d;' % remaining): guids[row['guid']] = 'yes' return guids.keys() 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) program.Download(db.GetSettingWithDefault('datadir', 'data')) 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' % (bytes / 1024) if bytes < 1024 * 1024 * 1024: return '%d mb' % (bytes / (1024 * 1024)) return '%d gb' % (bytes / (1024 * 1024 * 1024)) def GetPossible(array, index): """GetPossible -- get a value from an array, handling it's absense 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 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 MythIPTV' 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 for guid in NextDownloads(sys.argv[2]): DownloadAndImport(db, guid) # And now make sure there aren't any stragglers for guid in db.GetWaitingForImport(): program = IpTvProgram(db) program.Load(guid) program.Import() 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) program.Download(db.GetSettingWithDefault('datadir', 'data')) 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'): print 'Updating %s' % row['url'] 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]): print guid program = IpTvProgram(db) program.Load(guid) print ' %s: %s' %(program.GetTitle(), program.GetSubtitle()) print else: Usage()