Ticket #3282: mythburn.py

File mythburn.py, 134.1 KB (added by wligtenberg@…, 17 years ago)

My new and improved mythburn.py script

Line 
1# mythburn.py
2# The ported MythBurn scripts which feature:
3
4# Burning of recordings (including HDTV) and videos
5# of ANY format to DVDR.  Menus are created using themes
6# and are easily customised.
7
8# See mydata.xml for format of input file
9
10# spit2k1
11# 11 January 2006
12# 6 Feb 2006 - Added into CVS for the first time
13
14# paulh
15# 4 May 2006 - Added into mythtv svn
16
17#For this script to work you need to have...
18#Python2.3.5
19#python2.3-mysqldb
20#python2.3-imaging (PIL)
21#dvdauthor - v0.6.11
22#ffmpeg - 0.4.6
23#dvd+rw-tools - v5.21.4.10.8
24#cdrtools - v2.01
25
26#Optional (only needed for tcrequant)
27#transcode - v1.0.2
28
29#******************************************************************************
30#******************************************************************************
31#******************************************************************************
32
33# version of script - change after each update
34VERSION="0.1.20060910-1"
35
36
37##You can use this debug flag when testing out new themes
38##pick some small recordings, run them through as normal
39##set this variable to True and then re-run the scripts
40##the temp. files will not be deleted and it will run through
41##very much quicker!
42debug_secondrunthrough = False
43
44# default encoding profile to use
45defaultEncodingProfile = "SP"
46
47#*********************************************************************************
48#Dont change the stuff below!!
49#*********************************************************************************
50import os, string, socket, sys, getopt, traceback, signal
51import xml.dom.minidom
52import Image, ImageDraw, ImageFont
53import MySQLdb, codecs
54import time, datetime, tempfile
55from fcntl import ioctl
56from CDROM import CDROMEJECT
57from CDROM import CDROMCLOSETRAY
58
59# media types (should match the enum in mytharchivewizard.h)
60DVD_SL = 0
61DVD_DL = 1
62DVD_RW = 2
63FILE   = 3
64
65dvdPAL=(720,576)
66dvdNTSC=(720,480)
67dvdPALdpi=(75,80)
68dvdNTSCdpi=(81,72)
69
70dvdPALHalfD1="352x576"
71dvdNTSCHalfD1="352x480"
72dvdPALD1="%sx%s" % (dvdPAL[0],dvdPAL[1])
73dvdNTSCD1="%sx%s" % (dvdNTSC[0],dvdNTSC[1])
74
75#Single and dual layer recordable DVD free space in MBytes
76dvdrsize=(4482,8964)
77
78frameratePAL=25
79framerateNTSC=29.97
80
81#Just blank globals at startup
82temppath=""
83logpath=""
84scriptpath=""
85sharepath=""
86videopath=""
87recordingpath=""
88defaultsettings=""
89videomode=""
90gallerypath=""
91musicpath=""
92dateformat=""
93timeformat=""
94dbVersion=""
95preferredlang1=""
96preferredlang2=""
97useFIFO = True
98encodetoac3 = False
99alwaysRunMythtranscode = False
100copyremoteFiles = False
101
102#main menu aspect ratio (4:3 or 16:9)
103mainmenuAspectRatio = "16:9"
104
105#chapter menu aspect ratio (4:3, 16:9 or Video)
106#video means same aspect ratio as the video title
107chaptermenuAspectRatio = "Video"
108
109#default chapter length in seconds
110chapterLength = 5 * 60;
111
112#name of the default job file
113jobfile="mydata.xml"
114
115#progress log filename and file object
116progresslog = ""
117progressfile = open("/dev/null", 'w')
118
119#default location of DVD drive
120dvddrivepath = "/dev/dvd"
121
122#default option settings
123docreateiso = False
124doburn = True
125erasedvdrw = False
126mediatype = DVD_SL
127savefilename = ''
128
129configHostname = socket.gethostname()
130installPrefix = ""
131
132# job xml file
133jobDOM = None
134
135# theme xml file
136themeDOM = None
137themeName = ''
138
139#Maximum of 10 theme fonts
140themeFonts = [0,0,0,0,0,0,0,0,0,0]
141
142def write(text, progress=True):
143    """Simple place to channel all text output through"""
144    sys.stdout.write(text + "\n")
145    sys.stdout.flush()
146
147    if progress == True and progresslog != "":
148        progressfile.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n")
149        progressfile.flush()
150
151def fatalError(msg):
152    """Display an error message and exit app"""
153    write("*"*60)
154    write("ERROR: " + msg)
155    write("*"*60)
156    write("")
157    sys.exit(0)
158
159def getTempPath():
160    """This is the folder where all temporary files will be created."""
161    return temppath
162
163def getIntroPath():
164    """This is the folder where all intro files are located."""
165    return os.path.join(sharepath, "mytharchive", "intro")
166
167def getEncodingProfilePath():
168    """This is the folder where all encoder profile files are located."""
169    return os.path.join(sharepath, "mytharchive", "encoder_profiles")
170
171def getMysqlDBParameters():
172    global mysql_host
173    global mysql_user
174    global mysql_passwd
175    global mysql_db
176    global configHostname
177    global installPrefix
178
179    f = tempfile.NamedTemporaryFile();
180    result = os.spawnlp(os.P_WAIT, 'mytharchivehelper','mytharchivehelper',
181                        '-p', f.name)
182    if result <> 0:
183        write("Failed to run mytharchivehelper to get mysql database parameters! "
184              "Exit code: %d" % result)
185        if result == 254:
186            fatalError("Failed to init mythcontext.\n"
187                       "Please check the troubleshooting section of the README for ways to fix this error")
188
189    f.seek(0)
190    mysql_host = f.readline()[:-1]
191    mysql_user = f.readline()[:-1]
192    mysql_passwd = f.readline()[:-1]
193    mysql_db = f.readline()[:-1]
194    configHostname = f.readline()[:-1]
195    installPrefix = f.readline()[:-1]
196    f.close()
197    del f
198
199def getDatabaseConnection():
200    """Returns a mySQL connection to mythconverg database."""
201    return MySQLdb.connect(host=mysql_host, user=mysql_user, passwd=mysql_passwd, db=mysql_db)
202
203def doesFileExist(file):
204    """Returns true/false if a given file or path exists."""
205    return os.path.exists( file )
206
207def quoteFilename(filename):
208    filename = filename.replace('"', '\\"')
209    return '"%s"' % filename
210
211def getText(node):
212    """Returns the text contents from a given XML element."""
213    if node.childNodes.length>0:
214        return node.childNodes[0].data
215    else:
216        return ""
217
218def getThemeFile(theme,file):
219    """Find a theme file - first look in the specified theme directory then look in the
220       shared music and image directories"""
221    if os.path.exists(os.path.join(sharepath, "mytharchive", "themes", theme, file)):
222        return os.path.join(sharepath, "mytharchive", "themes", theme, file)
223
224    if os.path.exists(os.path.join(sharepath, "mytharchive", "images", file)):
225        return os.path.join(sharepath, "mytharchive", "images", file)
226
227    if os.path.exists(os.path.join(sharepath, "mytharchive", "music", file)):
228        return os.path.join(sharepath, "mytharchive", "music", file)
229
230    fatalError("Cannot find theme file '%s' in theme '%s'" % (file, theme))
231
232def getFontPathName(fontname):
233    return os.path.join(sharepath, fontname)
234
235def getItemTempPath(itemnumber):
236    return os.path.join(getTempPath(),"%s" % itemnumber)
237
238def validateTheme(theme):
239    #write( "Checking theme", theme
240    file = getThemeFile(theme,"theme.xml")
241    write("Looking for: " + file)
242    return doesFileExist( getThemeFile(theme,"theme.xml") )
243
244def isResolutionHDTV(videoresolution):
245    return (videoresolution[0]==1920 and videoresolution[1]==1080) or (videoresolution[0]==1280 and videoresolution[1]==720)
246
247def isResolutionOkayForDVD(videoresolution):
248    if videomode=="ntsc":
249        return videoresolution==(720,480) or videoresolution==(704,480) or videoresolution==(352,480) or videoresolution==(352,240)
250    else:
251        return videoresolution==(720,576) or videoresolution==(704,576) or videoresolution==(352,576) or videoresolution==(352,288)
252
253def getImageSize(sourcefile):
254    myimage=Image.open(sourcefile,"r")
255    return myimage.size
256
257def deleteAllFilesInFolder(folder):
258    """Does what it says on the tin!."""
259    for root, dirs, deletefiles in os.walk(folder, topdown=False):
260        for name in deletefiles:
261                os.remove(os.path.join(root, name))
262
263def checkCancelFlag():
264    """Checks to see if the user has cancelled this run"""
265    if os.path.exists(os.path.join(logpath, "mythburncancel.lck")):
266        os.remove(os.path.join(logpath, "mythburncancel.lck"))
267        write('*'*60)
268        write("Job has been cancelled at users request")
269        write('*'*60)
270        sys.exit(1)
271
272def runCommand(command):
273    checkCancelFlag()
274    result=os.system(command)
275    checkCancelFlag()
276    return result
277
278def encodeMenu(background, tempvideo, music, musiclength, tempmovie, xmlfile, finaloutput, aspectratio):
279    if videomode=="pal":
280        framespersecond=frameratePAL
281    else:
282        framespersecond=framerateNTSC
283
284    totalframes=int(musiclength * framespersecond)
285
286    command = path_png2yuv[0] + " -n %s -v0 -I p -f %s -j '%s' | %s -b 5000 -a %s -v 1 -f 8 -o '%s'" \
287               % (totalframes, framespersecond, background, path_mpeg2enc[0], aspectratio, tempvideo)
288    result = runCommand(command)
289    if result<>0:
290        fatalError("Failed while running png2yuv - %s" % command)
291
292    command = path_mplex[0] + " -f 8 -v 0 -o '%s' '%s' '%s'" % (tempmovie, tempvideo, music)
293    result = runCommand(command)
294    if result<>0:
295        fatalError("Failed while running mplex - %s" % command)
296
297    if xmlfile != "":
298        command = path_spumux[0] + " -m dvd -s 0 '%s' < '%s' > '%s'" % (xmlfile, tempmovie, finaloutput)
299        result = runCommand(command)
300        if result<>0:
301            fatalError("Failed while running spumux - %s" % command)
302    else:
303        os.rename(tempmovie, finaloutput)
304
305    if os.path.exists(tempvideo):
306            os.remove(tempvideo)
307    if os.path.exists(tempmovie):
308            os.remove(tempmovie)
309
310def findEncodingProfile(profile):
311    """Returns the XML node for the given encoding profile"""
312
313    # which encoding file do we need
314    if videomode == "ntsc":
315        filename = getEncodingProfilePath() + "/ffmpeg_dvd_ntsc.xml"
316    else:
317        filename = getEncodingProfilePath() + "/ffmpeg_dvd_pal.xml"
318
319    DOM = xml.dom.minidom.parse(filename)
320
321    #Error out if its the wrong XML
322    if DOM.documentElement.tagName != "encoderprofiles":
323        fatalError("Profile xml file doesn't look right (%s)" % filename)
324
325    profiles = DOM.getElementsByTagName("profile")
326    for node in profiles:
327        if getText(node.getElementsByTagName("name")[0]) == profile:
328            write("Encoding profile (%s) found" % profile)
329            return node
330
331    fatalError("Encoding profile (%s) not found" % profile)
332    return None
333
334def getThemeConfigurationXML(theme):
335    """Loads the XML file from disk for a specific theme"""
336
337    #Load XML input file from disk
338    themeDOM = xml.dom.minidom.parse( getThemeFile(theme,"theme.xml") )
339    #Error out if its the wrong XML
340    if themeDOM.documentElement.tagName != "mythburntheme":
341        fatalError("Theme xml file doesn't look right (%s)" % theme)
342    return themeDOM
343
344def getLengthOfVideo(index):
345    """Returns the length of a video file (in seconds)"""
346
347    #open the XML containing information about this file
348    infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
349
350    #error out if its the wrong XML
351    if infoDOM.documentElement.tagName != "file":
352        fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
353    file = infoDOM.getElementsByTagName("file")[0]
354    if file.attributes["duration"].value != 'N/A':
355        duration = int(file.attributes["duration"].value)
356    else:
357        duration = 0;
358
359    return duration
360
361def getAudioParams(folder):
362    """Returns the audio bitrate and no of channels for a file from its streaminfo.xml"""
363
364    #open the XML containing information about this file
365    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
366
367    #error out if its the wrong XML
368    if infoDOM.documentElement.tagName != "file":
369        fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
370    audio = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("audio")[0]
371
372    samplerate = audio.attributes["samplerate"].value
373    channels = audio.attributes["channels"].value
374
375    return (samplerate, channels)
376
377def getVideoParams(folder):
378    """Returns the video resolution, fps and aspect ratio for the video file from the streamindo.xml file"""
379
380    #open the XML containing information about this file
381    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
382
383    #error out if its the wrong XML
384    if infoDOM.documentElement.tagName != "file":
385        fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
386    video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
387
388    if video.attributes["aspectratio"].value != 'N/A':
389        aspect_ratio = video.attributes["aspectratio"].value
390    else:
391        aspect_ratio = "1.77778"
392
393    videores = video.attributes["width"].value + 'x' + video.attributes["height"].value
394    fps = video.attributes["fps"].value
395
396    return (videores, fps, aspect_ratio)
397
398def getAspectRatioOfVideo(index):
399    """Returns the aspect ratio of the video file (1.333, 1.778, etc)"""
400
401    #open the XML containing information about this file
402    infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
403
404    #error out if its the wrong XML
405    if infoDOM.documentElement.tagName != "file":
406        fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
407    video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
408    if video.attributes["aspectratio"].value != 'N/A':
409        aspect_ratio = float(video.attributes["aspectratio"].value)
410    else:
411        aspect_ratio = 1.77778; # default
412    write("aspect ratio is: %s" % aspect_ratio)
413    return aspect_ratio
414
415def getFormatedLengthOfVideo(index):
416    duration = getLengthOfVideo(index)
417
418    minutes = int(duration / 60)
419    seconds = duration % 60
420    hours = int(minutes / 60)
421    minutes %= 60
422
423    return '%02d:%02d:%02d' % (hours, minutes, seconds)
424
425def createVideoChapters(itemnum, numofchapters, lengthofvideo, getthumbnails):     
426    """Returns numofchapters chapter marks even spaced through a certain time period"""
427    segment=int(lengthofvideo / numofchapters)
428
429    write( "Video length is %s seconds. Each chapter will be %s seconds" % (lengthofvideo,segment))
430
431    chapters=""
432    thumbList=""
433    starttime=0
434    count=1
435    while count<=numofchapters:
436        chapters+=time.strftime("%H:%M:%S",time.gmtime(starttime))
437
438        thumbList+="%s," % starttime
439
440        if numofchapters>1:
441            chapters+=","
442        starttime+=segment
443        count+=1
444
445    if getthumbnails==True:
446        extractVideoFrames( os.path.join(getItemTempPath(itemnum),"stream.mv2"),
447            os.path.join(getItemTempPath(itemnum),"chapter-%1.jpg"), thumbList)
448
449    return chapters
450
451def createVideoChaptersFixedLength(segment, lengthofvideo):
452    """Returns chapter marks spaced segment seconds through the file"""
453    if lengthofvideo < segment:
454        return "00:00:00"
455
456    numofchapters = lengthofvideo / segment;
457    chapters = ""
458    starttime = 0
459    count = 1
460    while count <= numofchapters:
461        chapters += time.strftime("%H:%M:%S", time.gmtime(starttime)) + ","
462        starttime += segment
463        count += 1
464
465    return chapters
466
467def getDefaultParametersFromMythTVDB():
468    """Reads settings from MythTV database"""
469
470    write( "Obtaining MythTV settings from MySQL database for hostname " + configHostname)
471
472    #TVFormat is not dependant upon the hostname.
473    sqlstatement="""select value, data from settings where value in('DBSchemaVer')
474                    or (hostname='""" + configHostname + """' and value in(
475                        'RecordFilePrefix',
476                        'VideoStartupDir',
477                        'GalleryDir',
478                        'MusicLocation',
479                        'MythArchiveVideoFormat',
480                        'MythArchiveTempDir',
481                        'MythArchiveFfmpegCmd',
482                        'MythArchiveMplexCmd',
483                        'MythArchiveDvdauthorCmd',
484                        'MythArchiveMkisofsCmd',
485                        'MythArchiveTcrequantCmd',
486                        'MythArchiveMpg123Cmd',
487                        'MythArchiveProjectXCmd',
488                        'MythArchiveDVDLocation',
489                        'MythArchiveGrowisofsCmd',
490                        'MythArchivePng2yuvCmd',
491                        'MythArchiveSpumuxCmd',
492                        'MythArchiveMpeg2encCmd',
493                        'MythArchiveEncodeToAc3',
494                        'MythArchiveCopyRemoteFiles',
495                        'MythArchiveAlwaysUseMythTranscode',
496                        'MythArchiveUseFIFO',
497                        'MythArchiveMainMenuAR',
498                        'MythArchiveChapterMenuAR',
499                        'ISO639Language0',
500                        'ISO639Language1'
501                        )) order by value"""
502
503    #write( sqlstatement)
504
505    # connect
506    db = getDatabaseConnection()
507    # create a cursor
508    cursor = db.cursor()
509    # execute SQL statement
510    cursor.execute(sqlstatement)
511    # get the resultset as a tuple
512    result = cursor.fetchall()
513
514    db.close()
515    del db
516    del cursor
517
518    cfg = {}
519    for i in range(len(result)):
520       cfg[result[i][0]] = result[i][1]
521
522    #bail out if we can't find the temp dir setting
523    if not "MythArchiveTempDir" in cfg:
524        fatalError("Can't find the setting for the temp directory. \nHave you run setup in the frontend?")
525    return cfg
526
527def getOptions(options):
528    global doburn
529    global docreateiso
530    global erasedvdrw
531    global mediatype
532    global savefilename
533
534    if options.length == 0:
535        fatalError("Trying to read the options from the job file but none found?")
536    options = options[0]
537
538    doburn = options.attributes["doburn"].value != '0'
539    docreateiso = options.attributes["createiso"].value != '0'
540    erasedvdrw = options.attributes["erasedvdrw"].value != '0'
541    mediatype = int(options.attributes["mediatype"].value)
542    savefilename = options.attributes["savefilename"].value
543
544    write("Options - mediatype = %d, doburn = %d, createiso = %d, erasedvdrw = %d" \
545           % (mediatype, doburn, docreateiso, erasedvdrw))
546    write("          savefilename = '%s'" % savefilename)
547
548def getTimeDateFormats():
549    """Reads date and time settings from MythTV database and converts them into python date time formats"""
550
551    global dateformat
552    global timeformat
553
554    #DateFormat =       ddd MMM d
555    #ShortDateFormat = M/d
556    #TimeFormat = h:mm AP
557
558
559    write( "Obtaining date and time settings from MySQL database for hostname "+ configHostname)
560
561    #TVFormat is not dependant upon the hostname.
562    sqlstatement = """select value,data from settings where (hostname='"""  + configHostname \
563                        + """' and value in (
564                        'DateFormat',
565                        'ShortDateFormat',
566                        'TimeFormat'
567                        )) order by value"""
568
569    # connect
570    db = getDatabaseConnection()
571    # create a cursor
572    cursor = db.cursor()
573    # execute SQL statement
574    cursor.execute(sqlstatement)
575    # get the resultset as a tuple
576    result = cursor.fetchall()
577    #We must have exactly 3 rows returned or else we have some MythTV settings missing
578    if int(cursor.rowcount)!=3:
579        fatalError("Failed to get time formats from the DB")
580    db.close()
581    del db
582    del cursor
583
584    #Copy the results into a dictionary for easier use
585    mydict = {}
586    for i in range(len(result)):
587        mydict[result[i][0]] = result[i][1]
588
589    del result
590
591    #At present we ignore the date time formats from MythTV and default to these
592    #basically we need to convert the MythTV formats into Python formats
593    #spit2k1 - TO BE COMPLETED!!
594
595    #Date and time formats used to show recording times see full list at
596    #http://www.python.org/doc/current/lib/module-time.html
597    dateformat="%a %d %b %Y"    #Mon 15 Dec 2005
598    timeformat="%I:%M %p"       #8:15 pm
599
600
601def expandItemText(infoDOM, text, itemnumber, pagenumber, keynumber,chapternumber, chapterlist ):
602    """Replaces keywords in a string with variables from the XML and filesystem"""
603    text=string.replace(text,"%page","%s" % pagenumber)
604
605    #See if we can use the thumbnail/cover file for videos if there is one.
606    if getText( infoDOM.getElementsByTagName("coverfile")[0]) =="":
607        text=string.replace(text,"%thumbnail", os.path.join( getItemTempPath(itemnumber), "thumbnail.jpg"))
608    else:
609        text=string.replace(text,"%thumbnail", getText( infoDOM.getElementsByTagName("coverfile")[0]) )
610
611    text=string.replace(text,"%itemnumber","%s" % itemnumber )
612    text=string.replace(text,"%keynumber","%s" % keynumber )
613
614    text=string.replace(text,"%title",getText( infoDOM.getElementsByTagName("title")[0]) )
615    text=string.replace(text,"%subtitle",getText( infoDOM.getElementsByTagName("subtitle")[0]) )
616    text=string.replace(text,"%description",getText( infoDOM.getElementsByTagName("description")[0]) )
617    text=string.replace(text,"%type",getText( infoDOM.getElementsByTagName("type")[0]) )
618
619    text=string.replace(text,"%recordingdate",getText( infoDOM.getElementsByTagName("recordingdate")[0]) )
620    text=string.replace(text,"%recordingtime",getText( infoDOM.getElementsByTagName("recordingtime")[0]) )
621
622    text=string.replace(text,"%duration", getFormatedLengthOfVideo(itemnumber))
623
624    text=string.replace(text,"%myfolder",getThemeFile(themeName,""))
625
626    if chapternumber>0:
627        text=string.replace(text,"%chapternumber","%s" % chapternumber )
628        text=string.replace(text,"%chaptertime","%s" % chapterlist[chapternumber - 1] )
629        text=string.replace(text,"%chapterthumbnail", os.path.join( getItemTempPath(itemnumber), "chapter-%s.jpg" % chapternumber))
630
631    return text
632
633def getScaledAttribute(node, attribute):
634    """ Returns a value taken from attribute in node scaled for the current video mode"""
635
636    if videomode == "pal" or attribute == "x" or attribute == "w":
637        return int(node.attributes[attribute].value)
638    else:
639        return int(float(node.attributes[attribute].value) / 1.2)
640
641def intelliDraw(drawer,text,font,containerWidth):
642    """Based on http://mail.python.org/pipermail/image-sig/2004-December/003064.html"""
643    #Args:
644    #  drawer: Instance of "ImageDraw.Draw()"
645    #  text: string of long text to be wrapped
646    #  font: instance of ImageFont (I use .truetype)
647    #  containerWidth: number of pixels text lines have to fit into.
648
649    #write("containerWidth: %s" % containerWidth)
650    words = text.split()
651    lines = [] # prepare a return argument
652    lines.append(words)
653    finished = False
654    line = 0
655    while not finished:
656        thistext = lines[line]
657        newline = []
658        innerFinished = False
659        while not innerFinished:
660            #write( 'thistext: '+str(thistext))
661            #write("textWidth: %s" % drawer.textsize(' '.join(thistext),font)[0])
662
663            if drawer.textsize(' '.join(thistext),font)[0] > containerWidth:
664                # this is the heart of the algorithm: we pop words off the current
665                # sentence until the width is ok, then in the next outer loop
666                # we move on to the next sentence.
667                if str(thistext).find(' ') != -1:
668                    newline.insert(0,thistext.pop(-1))
669                else:
670                    # FIXME should truncate the string here
671                    innerFinished = True
672            else:
673                innerFinished = True
674        if len(newline) > 0:
675            lines.append(newline)
676            line = line + 1
677        else:
678            finished = True
679    tmp = []
680    for i in lines:
681        tmp.append( ' '.join(i) )
682    lines = tmp
683    (width,height) = drawer.textsize(lines[0],font)
684    return (lines,width,height)
685
686def paintText(draw, x, y, width, height, text, font, colour, alignment):
687    """Takes a piece of text and draws it onto an image inside a bounding box."""
688    #The text is wider than the width of the bounding box
689
690    lines,tmp,h = intelliDraw(draw,text,font,width)
691    j = 0
692
693    for i in lines:
694        if (j*h) < (height-h):
695            write( "Wrapped text  = " + i.encode("ascii", "replace"), False)
696
697            if alignment=="left":
698                indent=0
699            elif  alignment=="center" or alignment=="centre":
700                indent=(width/2) - (draw.textsize(i,font)[0] /2)
701            elif  alignment=="right":
702                indent=width - draw.textsize(i,font)[0]
703            else:
704                indent=0
705            draw.text( (x+indent,y+j*h),i , font=font, fill=colour)
706        else:
707            write( "Truncated text = " + i.encode("ascii", "replace"), False)
708        #Move to next line
709        j = j + 1
710
711def checkBoundaryBox(boundarybox, node):
712    # We work out how much space all of our graphics and text are taking up
713    # in a bounding rectangle so that we can use this as an automatic highlight
714    # on the DVD menu   
715    if getText(node.attributes["static"]) == "False":
716        if getScaledAttribute(node, "x") < boundarybox[0]:
717            boundarybox = getScaledAttribute(node, "x"), boundarybox[1], boundarybox[2], boundarybox[3]
718
719        if getScaledAttribute(node, "y") < boundarybox[1]:
720            boundarybox = boundarybox[0], getScaledAttribute(node, "y"), boundarybox[2], boundarybox[3]
721
722        if (getScaledAttribute(node, "x") + getScaledAttribute(node, "w")) > boundarybox[2]:
723            boundarybox = boundarybox[0], boundarybox[1], getScaledAttribute(node, "x") + \
724                          getScaledAttribute(node, "w"), boundarybox[3]
725
726        if (getScaledAttribute(node, "y") + getScaledAttribute(node, "h")) > boundarybox[3]:
727            boundarybox = boundarybox[0], boundarybox[1], boundarybox[2], \
728                          getScaledAttribute(node, "y") + getScaledAttribute(node, "h")
729
730    return boundarybox
731
732def loadFonts(themeDOM):
733    global themeFonts
734
735    #Find all the fonts
736    nodelistfonts=themeDOM.getElementsByTagName("font")
737
738    fontnumber=0
739    for nodefont in nodelistfonts:
740        fontname = getText(nodefont)
741        fontsize = getScaledAttribute(nodefont, "size")
742        themeFonts[fontnumber]=ImageFont.truetype(getFontPathName(fontname),fontsize )
743        write( "Loading font %s, %s size %s" % (fontnumber,getFontPathName(fontname),fontsize) )
744        fontnumber+=1
745
746def getFileInformation(file, outputfile):
747    impl = xml.dom.minidom.getDOMImplementation()
748    infoDOM = impl.createDocument(None, "fileinfo", None)
749    top_element = infoDOM.documentElement
750
751    # if the jobfile has amended file details use them
752    details = file.getElementsByTagName("details")
753    if details.length > 0:
754        node = infoDOM.createElement("type")
755        node.appendChild(infoDOM.createTextNode(file.attributes["type"].value))
756        top_element.appendChild(node)
757
758        node = infoDOM.createElement("filename")
759        node.appendChild(infoDOM.createTextNode(file.attributes["filename"].value))
760        top_element.appendChild(node)   
761
762        node = infoDOM.createElement("title")
763        node.appendChild(infoDOM.createTextNode(details[0].attributes["title"].value))
764        top_element.appendChild(node)
765
766        node = infoDOM.createElement("recordingdate")
767        node.appendChild(infoDOM.createTextNode(details[0].attributes["startdate"].value))
768        top_element.appendChild(node)
769
770        node = infoDOM.createElement("recordingtime")
771        node.appendChild(infoDOM.createTextNode(details[0].attributes["starttime"].value))
772        top_element.appendChild(node)   
773
774        node = infoDOM.createElement("subtitle")
775        node.appendChild(infoDOM.createTextNode(details[0].attributes["subtitle"].value))
776        top_element.appendChild(node)   
777
778        node = infoDOM.createElement("description")
779        node.appendChild(infoDOM.createTextNode(getText(details[0])))
780        top_element.appendChild(node)   
781
782        node = infoDOM.createElement("rating")
783        node.appendChild(infoDOM.createTextNode(""))
784        top_element.appendChild(node)   
785
786        node = infoDOM.createElement("coverfile")
787        node.appendChild(infoDOM.createTextNode(""))
788        top_element.appendChild(node)
789
790        #FIXME: add cutlist to details?
791        node = infoDOM.createElement("cutlist")
792        node.appendChild(infoDOM.createTextNode(""))
793        top_element.appendChild(node)
794
795    #recorded table contains
796    #progstart, stars, cutlist, category, description, subtitle, title, chanid
797    #2005-12-20 00:00:00, 0.0,
798    elif file.attributes["type"].value=="recording":
799        sqlstatement  = """SELECT progstart, stars, cutlist, category, description, subtitle,
800                           title, starttime, chanid
801                           FROM recorded WHERE basename = '%s'""" % file.attributes["filename"].value.replace("'", "\\'")
802
803        # connect
804        db = getDatabaseConnection()
805        # create a cursor
806        cursor = db.cursor()
807        # execute SQL statement
808        cursor.execute(sqlstatement)
809        # get the resultset as a tuple
810        result = cursor.fetchall()
811        # get the number of rows in the resultset
812        numrows = int(cursor.rowcount)
813        #We must have exactly 1 row returned for this recording
814        if numrows!=1:
815            fatalError("Failed to get recording details from the DB for %s" % file.attributes["filename"].value)
816
817        # iterate through resultset
818        for record in result:
819            #write( record[0] , "-->", record[1], record[2], record[3])
820            write( "          " + record[6])
821            #Create an XML DOM to hold information about this video file
822
823            node = infoDOM.createElement("type")
824            node.appendChild(infoDOM.createTextNode(file.attributes["type"].value))
825            top_element.appendChild(node)
826
827            node = infoDOM.createElement("filename")
828            node.appendChild(infoDOM.createTextNode(file.attributes["filename"].value))
829            top_element.appendChild(node)   
830
831            node = infoDOM.createElement("title")
832            node.appendChild(infoDOM.createTextNode(unicode(record[6], "UTF-8")))
833            top_element.appendChild(node)
834
835            #date time is returned as 2005-12-19 00:15:00           
836            recdate=time.strptime( "%s" % record[0],"%Y-%m-%d %H:%M:%S")
837
838            node = infoDOM.createElement("recordingdate")
839            node.appendChild(infoDOM.createTextNode( time.strftime(dateformat,recdate)  ))
840            top_element.appendChild(node)
841
842            node = infoDOM.createElement("recordingtime")
843            node.appendChild(infoDOM.createTextNode( time.strftime(timeformat,recdate)))
844            top_element.appendChild(node)   
845
846            node = infoDOM.createElement("subtitle")
847            node.appendChild(infoDOM.createTextNode(unicode(record[5], "UTF-8")))
848            top_element.appendChild(node)   
849
850            node = infoDOM.createElement("description")
851            node.appendChild(infoDOM.createTextNode(unicode(record[4], "UTF-8")))
852            top_element.appendChild(node)   
853
854            node = infoDOM.createElement("rating")
855            node.appendChild(infoDOM.createTextNode("%s" % record[1]))
856            top_element.appendChild(node)   
857
858            node = infoDOM.createElement("coverfile")
859            node.appendChild(infoDOM.createTextNode(""))
860            #node.appendChild(infoDOM.createTextNode(record[8]))
861            top_element.appendChild(node)
862
863            node = infoDOM.createElement("chanid")
864            node.appendChild(infoDOM.createTextNode("%s" % record[8]))
865            top_element.appendChild(node)
866
867            #date time is returned as 2005-12-19 00:15:00
868            recdate=time.strptime( "%s" % record[7],"%Y-%m-%d %H:%M:%S")
869
870            node = infoDOM.createElement("starttime")
871            node.appendChild(infoDOM.createTextNode( time.strftime("%Y-%m-%dT%H:%M:%S", recdate)))
872            top_element.appendChild(node)
873
874            starttime = record[7]
875            chanid = record[8]
876
877            # find the cutlist if available
878            sqlstatement  = """SELECT mark, type FROM recordedmarkup
879                               WHERE chanid = '%s' AND starttime = '%s'
880                               AND type IN (0,1) ORDER BY mark""" % (chanid, starttime)
881            cursor = db.cursor()
882            # execute SQL statement
883            cursor.execute(sqlstatement)
884            if cursor.rowcount > 0:
885                node = infoDOM.createElement("hascutlist")
886                node.appendChild(infoDOM.createTextNode("yes"))
887                top_element.appendChild(node)
888            else:
889                node = infoDOM.createElement("hascutlist")
890                node.appendChild(infoDOM.createTextNode("no"))
891                top_element.appendChild(node)
892
893        db.close()
894        del db
895        del cursor
896
897    elif file.attributes["type"].value=="video":
898        filename = os.path.join(videopath, file.attributes["filename"].value.replace("'", "\\'"))
899        sqlstatement="""select title, director, plot, rating, inetref, year,
900                        userrating, length, coverfile from videometadata
901                        where filename='%s'""" % filename
902
903        # connect
904        db = getDatabaseConnection()
905        # create a cursor
906        cursor = db.cursor()
907        # execute SQL statement
908        cursor.execute(sqlstatement)
909        # get the resultset as a tuple
910        result = cursor.fetchall()
911        # get the number of rows in the resultset
912        numrows = int(cursor.rowcount)
913
914        #title,director,plot,rating,inetref,year,userrating,length,coverfile
915        #We must have exactly 1 row returned for this recording
916        if numrows<>1:
917            #Theres no record in the database so use a dummy row so we dont die!
918            #title,director,plot,rating,inetref,year,userrating,length,coverfile
919            record = file.attributes["filename"].value, "","",0,"","",0,0,""
920
921        for record in result:
922            write( "          " + record[0])
923
924            node = infoDOM.createElement("type")
925            node.appendChild(infoDOM.createTextNode(file.attributes["type"].value))
926            top_element.appendChild(node)
927
928            node = infoDOM.createElement("filename")
929            node.appendChild(infoDOM.createTextNode(file.attributes["filename"].value))
930            top_element.appendChild(node)   
931
932            node = infoDOM.createElement("title")
933            node.appendChild(infoDOM.createTextNode(unicode(record[0], "UTF-8")))
934            top_element.appendChild(node)   
935
936            node = infoDOM.createElement("recordingdate")
937            date = int(record[5])
938            if date != 1895:
939                node.appendChild(infoDOM.createTextNode("%s" % record[5]))
940            else:
941                node.appendChild(infoDOM.createTextNode(""))
942
943            top_element.appendChild(node)
944
945            node = infoDOM.createElement("recordingtime")
946            #node.appendChild(infoDOM.createTextNode(""))
947            top_element.appendChild(node)   
948
949            node = infoDOM.createElement("subtitle")
950            #node.appendChild(infoDOM.createTextNode(""))
951            top_element.appendChild(node)   
952
953            node = infoDOM.createElement("description")
954            desc = unicode(record[2], "UTF-8")
955            if desc != "None":
956                node.appendChild(infoDOM.createTextNode(desc))
957            else:
958                node.appendChild(infoDOM.createTextNode(""))
959
960            top_element.appendChild(node)
961
962            node = infoDOM.createElement("rating")
963            node.appendChild(infoDOM.createTextNode("%s" % record[6]))
964            top_element.appendChild(node)   
965
966            node = infoDOM.createElement("cutlist")
967            #node.appendChild(infoDOM.createTextNode(record[2]))
968            top_element.appendChild(node)   
969
970            node = infoDOM.createElement("coverfile")
971            if doesFileExist(record[8]):
972                node.appendChild(infoDOM.createTextNode(record[8]))
973            else:
974                node.appendChild(infoDOM.createTextNode(""))
975            top_element.appendChild(node)
976
977        db.close()
978        del db
979        del cursor
980
981    elif file.attributes["type"].value=="file":
982
983        node = infoDOM.createElement("type")
984        node.appendChild(infoDOM.createTextNode(file.attributes["type"].value))
985        top_element.appendChild(node)
986
987        node = infoDOM.createElement("filename")
988        node.appendChild(infoDOM.createTextNode(file.attributes["filename"].value))
989        top_element.appendChild(node)
990
991        node = infoDOM.createElement("title")
992        node.appendChild(infoDOM.createTextNode(file.attributes["filename"].value))
993        top_element.appendChild(node)
994
995        node = infoDOM.createElement("recordingdate")
996        node.appendChild(infoDOM.createTextNode(""))
997        top_element.appendChild(node)
998
999        node = infoDOM.createElement("recordingtime")
1000        node.appendChild(infoDOM.createTextNode(""))
1001        top_element.appendChild(node)   
1002
1003        node = infoDOM.createElement("subtitle")
1004        node.appendChild(infoDOM.createTextNode(""))
1005        top_element.appendChild(node)   
1006
1007        node = infoDOM.createElement("description")
1008        node.appendChild(infoDOM.createTextNode(""))
1009        top_element.appendChild(node)   
1010
1011        node = infoDOM.createElement("rating")
1012        node.appendChild(infoDOM.createTextNode(""))
1013        top_element.appendChild(node)   
1014
1015        node = infoDOM.createElement("cutlist")
1016        node.appendChild(infoDOM.createTextNode(""))
1017        top_element.appendChild(node)   
1018
1019        node = infoDOM.createElement("coverfile")
1020        node.appendChild(infoDOM.createTextNode(""))
1021        top_element.appendChild(node)   
1022
1023    WriteXMLToFile (infoDOM, outputfile)
1024
1025def WriteXMLToFile(myDOM, filename):
1026    #Save the XML file to disk for use later on
1027    f=open(filename, 'w')
1028    f.write(myDOM.toxml("UTF-8"))
1029    f.close()
1030
1031
1032def preProcessFile(file, folder):
1033    """Pre-process a single video/recording file."""
1034
1035    write( "Pre-processing file '" + file.attributes["filename"].value + \
1036           "' of type '"+ file.attributes["type"].value+"'")
1037
1038    #As part of this routine we need to pre-process the video:
1039    #1. check the file actually exists
1040    #2. extract information from mythtv for this file in xml file
1041    #3. Extract a single frame from the video to use as a thumbnail and resolution check
1042    mediafile=""
1043
1044    if file.attributes["type"].value == "recording":
1045        mediafile = os.path.join(recordingpath, file.attributes["filename"].value)
1046    elif file.attributes["type"].value == "video":
1047        mediafile = os.path.join(videopath, file.attributes["filename"].value)
1048    elif file.attributes["type"].value == "file":
1049        mediafile = file.attributes["filename"].value
1050    else:
1051        fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
1052
1053    if doesFileExist(mediafile) == False:
1054        fatalError("Source file does not exist: " + mediafile)
1055
1056    if file.hasAttribute("localfilename"):
1057        mediafile = file.attributes["localfilename"].value
1058
1059    #write( "Original file is",os.path.getsize(mediafile),"bytes in size")
1060    getFileInformation(file, os.path.join(folder, "info.xml"))
1061
1062    getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
1063
1064    videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
1065
1066    write( "Video resolution is %s by %s" % (videosize[0], videosize[1]))
1067
1068def encodeAudio(format, sourcefile, destinationfile, deletesourceafterencode):
1069    write( "Encoding audio to "+format)
1070    if format=="ac3":
1071        cmd=path_ffmpeg[0] + " -v 0 -y -i '%s' -f ac3 -ab 192 -ar 48000 '%s'" % (sourcefile, destinationfile)
1072        result=runCommand(cmd)
1073
1074        if result!=0:
1075            fatalError("Failed while running ffmpeg to re-encode the audio to ac3\n"
1076                       "Command was %s" % cmd)
1077    else:
1078        fatalError("Unknown encodeAudio format " + format)
1079
1080    if deletesourceafterencode==True:
1081        os.remove(sourcefile)
1082
1083def multiplexMPEGStream(video, audio1, audio2, destination):
1084    """multiplex one video and one or two audio streams together"""
1085
1086    write("Multiplexing MPEG stream to %s" % destination)
1087
1088    if doesFileExist(destination)==True:
1089        os.remove(destination)
1090
1091    if useFIFO==True:
1092        os.mkfifo(destination)
1093        mode=os.P_NOWAIT
1094    else:
1095        mode=os.P_WAIT
1096
1097    checkCancelFlag()
1098
1099    if not doesFileExist(audio2):
1100        write("Available streams - video and one audio stream")
1101        result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
1102                    '-f', '8',
1103                    '-v', '0',
1104                    '-o', destination,
1105                    video,
1106                    audio1)
1107    else:
1108        write("Available streams - video and two audio streams")
1109        result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
1110                    '-f', '8',
1111                    '-v', '0',
1112                    '-o', destination,
1113                    video,
1114                    audio1,
1115                    audio2)
1116
1117    if useFIFO==True:
1118        write( "Multiplex started PID=%s" % result)
1119        return result
1120    else:
1121        if result != 0:
1122            fatalError("mplex failed with result %d" % result)
1123
1124def getStreamInformation(filename, xmlFilename, lenMethod):
1125    """create a stream.xml file for filename"""
1126    filename = quoteFilename(filename)
1127    command = "mytharchivehelper -i %s %s %d" % (filename, xmlFilename, lenMethod)
1128
1129    result = runCommand(command)
1130
1131    if result <> 0:
1132        fatalError("Failed while running mytharchivehelper to get stream information from %s" % filename)
1133
1134def getVideoSize(xmlFilename):
1135    """Get video width and height from stream.xml file"""
1136
1137    #open the XML containing information about this file
1138    infoDOM = xml.dom.minidom.parse(xmlFilename)
1139    #error out if its the wrong XML
1140   
1141    if infoDOM.documentElement.tagName != "file":
1142        fatalError("This info file doesn't look right (%s)." % xmlFilename)
1143    nodes = infoDOM.getElementsByTagName("video")
1144    if nodes.length == 0:
1145        fatalError("Didn't find any video elements in stream info file. (%s)" % xmlFilename)
1146
1147    if nodes.length > 1:
1148        write("Found more than one video element in stream info file.!!!")
1149    node = nodes[0]
1150    width = int(node.attributes["width"].value)
1151    height = int(node.attributes["height"].value)
1152
1153    return (width, height)
1154
1155def runMythtranscode(chanid, starttime, destination, usecutlist, localfile):
1156    """Use mythtrancode to cut commercials and/or clean up an mpeg2 file"""
1157
1158    if localfile != "":
1159        localfile = quoteFilename(localfile)
1160        if usecutlist == True:
1161            command = "mythtranscode --mpeg2 --honorcutlist -i %s -o %s" % (localfile, destination)
1162        else:
1163            command = "mythtranscode --mpeg2 -i %s -o %s" % (localfile, destination)
1164    else:
1165        if usecutlist == True:
1166            command = "mythtranscode --mpeg2 --honorcutlist -c %s -s %s -o %s" % (chanid, starttime, destination)
1167        else:
1168            command = "mythtranscode --mpeg2 -c %s -s %s -o %s" % (chanid, starttime, destination)
1169
1170    result = runCommand(command)
1171
1172    if (result != 0):
1173        write("Failed while running mythtranscode to cut commercials and/or clean up an mpeg2 file.\n"
1174              "Result: %d, Command was %s" % (result, command))
1175        return False;
1176
1177    return True
1178
1179def extractVideoFrame(source, destination, seconds):
1180    write("Extracting thumbnail image from %s at position %s" % (source, seconds))
1181    write("Destination file %s" % destination)
1182
1183    if doesFileExist(destination) == False:
1184
1185        if videomode=="pal":
1186            fr=frameratePAL
1187        else:
1188            fr=framerateNTSC
1189
1190        source = quoteFilename(source)
1191
1192        command = "mytharchivehelper -t  %s '%s' %s" % (source, seconds, destination)
1193        result = runCommand(command)
1194        if result <> 0:
1195            fatalError("Failed while running mytharchivehelper to get thumbnails.\n"
1196                       "Result: %d, Command was %s" % (result, command))
1197    try:
1198        myimage=Image.open(destination,"r")
1199
1200        if myimage.format <> "JPEG":
1201            write( "Something went wrong with thumbnail capture - " + myimage.format)
1202            return (0L,0L)
1203        else:
1204            return myimage.size
1205    except IOError:
1206        return (0L, 0L)
1207
1208def extractVideoFrames(source, destination, thumbList):
1209    write("Extracting thumbnail images from: %s - at %s" % (source, thumbList))
1210    write("Destination file %s" % destination)
1211
1212    source = quoteFilename(source)
1213
1214    command = "mytharchivehelper -v important -t %s '%s' %s" % (source, thumbList, destination)
1215    result = runCommand(command)
1216    if result <> 0:
1217        fatalError("Failed while running mytharchivehelper to get thumbnails")
1218
1219def encodeVideoToMPEG2(source, destvideofile, video, audio1, audio2, aspectratio, profile):
1220    """Encodes an unknown video source file eg. AVI to MPEG2 video and AC3 audio, use ffmpeg"""
1221
1222    profileNode = findEncodingProfile(profile)
1223
1224    passes = int(getText(profileNode.getElementsByTagName("passes")[0]))
1225
1226    command = path_ffmpeg[0]
1227
1228    parameters = profileNode.getElementsByTagName("parameter")
1229
1230    for param in parameters:
1231        name = param.attributes["name"].value
1232        value = param.attributes["value"].value
1233
1234        # do some parameter substitution
1235        if value == "%inputfile":
1236            value = quoteFilename(source)
1237        if value == "%outputfile":
1238            value = quoteFilename(destvideofile)
1239        if value == "%aspect":
1240            value = aspectratio
1241
1242        # only re-encode the audio if it is not already in AC3 format
1243        if audio1[AUDIO_CODEC] == "AC3":
1244            if name == "-acodec":
1245                value = "copy"
1246            if name == "-ar" or name == "-ab" or name == "-ac":
1247                name = ""
1248                value = ""
1249
1250        if name != "":
1251            command += " " + name
1252
1253        if value != "":
1254            command += " " + value
1255
1256
1257    #add second audio track if required
1258    if audio2[AUDIO_ID] != -1:
1259        command += " -newaudio"
1260
1261    #make sure we get the correct stream(s) that we want
1262    command += " -map 0:%d -map 0:%d " % (video[VIDEO_INDEX], audio1[AUDIO_INDEX])
1263    if audio2[AUDIO_ID] != -1:
1264        command += "-map 0:%d" % (audio2[AUDIO_INDEX])
1265
1266    if passes == 1:
1267        write(command)
1268        result = runCommand(command)
1269        if result!=0:
1270            fatalError("Failed while running ffmpeg to re-encode video.\n"
1271                       "Command was %s" % command)
1272
1273    else:
1274        passLog = os.path.join(getTempPath(), 'pass.log')
1275
1276        pass1 = string.replace(command, "%pass","1")
1277        pass1 = string.replace(pass1, "%passlogfile", passLog)
1278        write("Pass 1 - " + pass1)
1279        result = runCommand(pass1)
1280
1281        if result!=0:
1282            fatalError("Failed while running ffmpeg (Pass 1) to re-encode video.\n"
1283                       "Command was %s" % command)
1284
1285        if os.path.exists(destvideofile):
1286            os.remove(destvideofile)
1287
1288        pass2 = string.replace(command, "%pass","2")
1289        pass2 = string.replace(pass2, "%passlogfile", passLog)
1290        write("Pass 2 - " + pass2)
1291        result = runCommand(pass2)
1292
1293        if result!=0:
1294            fatalError("Failed while running ffmpeg (Pass 2) to re-encode video.\n"
1295                       "Command was %s" % command)
1296
1297def encodeNuvToMPEG2(chanid, starttime, destvideofile, folder, profile, usecutlist):
1298    """Encodes a nuv video source file to MPEG2 video and AC3 audio, using mythtranscode & ffmpeg"""
1299
1300    # make sure mythtranscode hasn't left some stale fifos hanging around
1301    if ((doesFileExist(os.path.join(folder, "audout")) or doesFileExist(os.path.join(folder, "vidout")))):
1302        fatalError("Something is wrong! Found one or more stale fifo's from mythtranscode\n"
1303                   "Delete the fifos in '%s' and start again" % folder)
1304
1305    profileNode = findEncodingProfile(profile)
1306    parameters = profileNode.getElementsByTagName("parameter")
1307
1308    # default values - will be overriden by values from the profile
1309    outvideobitrate = 5000
1310    if videomode == "ntsc":
1311        outvideores = "720x480"
1312    else:
1313        outvideores = "720x576"
1314
1315    outaudiochannels = 2
1316    outaudiobitrate = 384
1317    outaudiosamplerate = 48000
1318    outaudiocodec = "ac3"
1319
1320    for param in parameters:
1321        name = param.attributes["name"].value
1322        value = param.attributes["value"].value
1323
1324        # we only support a subset of the parameter for the moment
1325        if name == "-acodec":
1326            outaudiocodec = value
1327        if name == "-ac":
1328            outaudiochannels = value
1329        if name == "-ab":
1330            outaudiobitrate = value
1331        if name == "-ar":
1332            outaudiosamplerate = value
1333        if name == "-b":
1334            outvideobitrate = value
1335        if name == "-s":
1336            outvideores = value
1337
1338    if (usecutlist == True):
1339        PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
1340                    '-p', '27',
1341                    '-c', chanid,
1342                    '-s', starttime,
1343                    '--honorcutlist',
1344                    '-f', folder)
1345        write("mythtranscode started (using cut list) PID = %s" % PID)
1346    else:
1347        PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
1348                    '-p', '27',
1349                    '-c', chanid,
1350                    '-s', starttime,
1351                    '-f', folder)
1352
1353        write("mythtranscode started PID = %s" % PID)
1354
1355
1356    samplerate, channels = getAudioParams(folder)
1357    videores, fps, aspectratio = getVideoParams(folder)
1358
1359    command =  path_ffmpeg[0] + " -y "
1360    command += "-f s16le -ar %s -ac %s -i %s " % (samplerate, channels, os.path.join(folder, "audout"))
1361    command += "-f rawvideo -pix_fmt yuv420p -s %s -aspect %s -r %s " % (videores, aspectratio, fps)
1362    command += "-i %s " % os.path.join(folder, "vidout")
1363    command += "-aspect %s -r %s -s %s -b %s " % (aspectratio, fps, outvideores, outvideobitrate)
1364    command += "-vcodec mpeg2video -qmin 5 "
1365    command += "-ab %s -ar %s -acodec %s " % (outaudiobitrate, outaudiosamplerate, outaudiocodec)
1366    command += "-f dvd %s" % quoteFilename(destvideofile)
1367
1368    #wait for mythtranscode to create the fifos
1369    tries = 30
1370    while (tries and not(doesFileExist(os.path.join(folder, "audout")) and
1371                         doesFileExist(os.path.join(folder, "vidout")))):
1372        tries -= 1
1373        write("Waiting for mythtranscode to create the fifos")
1374        time.sleep(1)
1375
1376    if (not(doesFileExist(os.path.join(folder, "audout")) and doesFileExist(os.path.join(folder, "vidout")))):
1377        fatalError("Waited too long for mythtranscode to create the fifos - giving up!!")
1378
1379    write("Running ffmpeg")
1380    result = runCommand(command)
1381    if result != 0:
1382        os.kill(PID, signal.SIGKILL)
1383        fatalError("Failed while running ffmpeg to re-encode video.\n"
1384                   "Command was %s" % command)
1385
1386def runDVDAuthor():
1387    write( "Starting dvdauthor")
1388    checkCancelFlag()
1389    result=os.spawnlp(os.P_WAIT, path_dvdauthor[0],path_dvdauthor[1],'-x',os.path.join(getTempPath(),'dvdauthor.xml'))
1390    if result<>0:
1391        fatalError("Failed while running dvdauthor. Result: %d" % result)
1392    write( "Finished  dvdauthor")
1393
1394def CreateDVDISO():
1395    write("Creating ISO image")
1396    checkCancelFlag()
1397    result = os.spawnlp(os.P_WAIT, path_mkisofs[0], path_mkisofs[1], '-dvd-video', \
1398        '-V','MythTV BurnDVD','-o',os.path.join(getTempPath(),'mythburn.iso'), \
1399        os.path.join(getTempPath(),'dvd'))
1400
1401    if result<>0:
1402        fatalError("Failed while running mkisofs.")
1403
1404    write("Finished creating ISO image")
1405
1406def BurnDVDISO():
1407    write( "Burning ISO image to %s" % dvddrivepath)
1408    checkCancelFlag()
1409
1410    if mediatype == DVD_RW and erasedvdrw == True:
1411        command = path_growisofs[0] + " -dvd-compat -use-the-force-luke -Z " + dvddrivepath + \
1412                  " -dvd-video -V 'MythTV BurnDVD' " + os.path.join(getTempPath(),'dvd')
1413    else:
1414        command = path_growisofs[0] + " -dvd-compat -Z " + dvddrivepath + \
1415                  " -dvd-video -V 'MythTV BurnDVD' " + os.path.join(getTempPath(),'dvd')
1416
1417    if os.system(command) != 0:
1418        write("ERROR: Retrying to start growisofs after reload.")
1419        f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
1420        r = ioctl(f,CDROMEJECT, 0)
1421        os.close(f)
1422        f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
1423        r = ioctl(f,CDROMCLOSETRAY, 0)
1424        os.close(f)
1425        result = os.system(command)
1426        if result != 0:
1427            write("-"*60)
1428            write("ERROR: Failed while running growisofs")
1429            write("Result %d, Command was: %s" % (result, command))
1430            write("Please check the troubleshooting section of the README for ways to fix this error")
1431            write("-"*60)
1432            write("")
1433            sys.exit(1)
1434
1435    # eject the burned disc
1436    f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
1437    r = ioctl(f,CDROMEJECT, 0)
1438    os.close(f)
1439
1440    write("Finished burning ISO image")
1441
1442def deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2):
1443    checkCancelFlag()
1444
1445    if getFileType(folder) == "mpegts":
1446        command = "mythreplex --demux --fix_sync -t TS -o %s " % (folder + "/stream")
1447        command += "-v %d " % (video[VIDEO_ID])
1448
1449        if audio1[AUDIO_ID] != -1:
1450            if audio1[AUDIO_CODEC] == 'MP2':
1451                command += "-a %d " % (audio1[AUDIO_ID])
1452            elif audio1[AUDIO_CODEC] == 'AC3':
1453                command += "-c %d " % (audio1[AUDIO_ID])
1454
1455        if audio2[AUDIO_ID] != -1:
1456            if audio2[AUDIO_CODEC] == 'MP2':
1457                command += "-a %d " % (audio2[AUDIO_ID])
1458            elif audio2[AUDIO_CODEC] == 'AC3':
1459                command += "-c %d " % (audio2[AUDIO_ID])
1460
1461    else:
1462        command = "mythreplex --demux  --fix_sync -o %s " % (folder + "/stream")
1463        command += "-v %d " % (video[VIDEO_ID] & 255)
1464
1465        if audio1[AUDIO_ID] != -1:
1466            if audio1[AUDIO_CODEC] == 'MP2':
1467                command += "-a %d " % (audio1[AUDIO_ID] & 255)
1468            elif audio1[AUDIO_CODEC] == 'AC3':
1469                command += "-c %d " % (audio1[AUDIO_ID] & 255)
1470
1471        if audio2[AUDIO_ID] != -1:
1472            if audio2[AUDIO_CODEC] == 'MP2':
1473                command += "-a %d " % (audio2[AUDIO_ID] & 255)
1474            elif audio2[AUDIO_CODEC] == 'AC3':
1475                command += "-c %d " % (audio2[AUDIO_ID] & 255)
1476
1477    mediafile = quoteFilename(mediafile)
1478    command += mediafile
1479    write("Running: " + command)
1480
1481    result = os.system(command)
1482
1483    if result<>0:
1484        fatalError("Failed while running mythreplex. Command was %s" % command)
1485
1486def runTcrequant(source,destination,percentage):
1487    checkCancelFlag()
1488
1489    write (path_tcrequant[0] + " %s %s %s" % (source,destination,percentage))
1490    result=os.spawnlp(os.P_WAIT, path_tcrequant[0],path_tcrequant[1],
1491            "-i",source,
1492            "-o",destination,
1493            "-d","2",
1494            "-f","%s" % percentage)
1495    if result<>0:
1496        fatalError("Failed while running tcrequant")
1497
1498def calculateFileSizes(files):
1499    """ Returns the sizes of all video, audio and menu files"""
1500    filecount=0
1501    totalvideosize=0
1502    totalaudiosize=0
1503    totalmenusize=0
1504
1505    for node in files:
1506        filecount+=1
1507        #Generate a temp folder name for this file
1508        folder=getItemTempPath(filecount)
1509        #Process this file
1510        file=os.path.join(folder,"stream.mv2")
1511        #Get size of video in MBytes
1512        totalvideosize+=os.path.getsize(file) / 1024 / 1024
1513        #Get size of audio track 1
1514        totalaudiosize+=os.path.getsize(os.path.join(folder,"stream0.ac3")) / 1024 / 1024
1515        #Get size of audio track 2 if available
1516        if doesFileExist(os.path.join(folder,"stream1.ac3")):
1517            totalaudiosize+=os.path.getsize(os.path.join(folder,"stream1.ac3")) / 1024 / 1024
1518        if doesFileExist(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount)):
1519            totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount)) / 1024 / 1024
1520
1521    filecount=1
1522    while doesFileExist(os.path.join(getTempPath(),"menu-%s.mpg" % filecount)):
1523        totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"menu-%s.mpg" % filecount)) / 1024 / 1024
1524        filecount+=1
1525
1526    return totalvideosize,totalaudiosize,totalmenusize
1527
1528def performMPEG2Shrink(files,dvdrsize):
1529    checkCancelFlag()
1530
1531    totalvideosize,totalaudiosize,totalmenusize=calculateFileSizes(files)
1532
1533    #Report findings
1534    write( "Total size of video files, before multiplexing, is %s Mbytes, audio is %s MBytes, menus are %s MBytes." % (totalvideosize,totalaudiosize,totalmenusize))
1535
1536    #Subtract the audio and menus from the size of the disk (we cannot shrink this further)
1537    dvdrsize-=totalaudiosize
1538    dvdrsize-=totalmenusize
1539
1540    #Add a little bit for the multiplexing stream data
1541    totalvideosize=totalvideosize*1.05
1542
1543    if dvdrsize<0:
1544        fatalError("Audio and menu files are greater than the size of a recordable DVD disk.  Giving up!")
1545
1546    if totalvideosize>dvdrsize:
1547        write( "Need to shrink MPEG2 video files to fit onto recordable DVD, video is %s MBytes too big." % (totalvideosize - dvdrsize))
1548        scalepercentage=totalvideosize/dvdrsize
1549        write( "Need to scale by %s" % scalepercentage)
1550
1551        if scalepercentage>3:
1552            write( "Large scale to shrink, may not work!")
1553
1554        #tcrequant (transcode) is an optional install so may not be available
1555        if path_tcrequant[0] == "":
1556            fatalError("tcrequant is not available to resize the files.  Giving up!")
1557
1558        filecount=0
1559        for node in files:
1560            filecount+=1
1561            runTcrequant(os.path.join(getItemTempPath(filecount),"stream.mv2"),os.path.join(getItemTempPath(filecount),"video.small.m2v"),scalepercentage)
1562            os.remove(os.path.join(getItemTempPath(filecount),"stream.mv2"))
1563            os.rename(os.path.join(getItemTempPath(filecount),"video.small.m2v"),os.path.join(getItemTempPath(filecount),"stream.mv2"))
1564
1565        totalvideosize,totalaudiosize,totalmenusize=calculateFileSizes(files)       
1566        write( "Total DVD size AFTER TCREQUANT is %s MBytes" % (totalaudiosize + totalmenusize + (totalvideosize*1.05)))
1567
1568    else:
1569        dvdrsize-=totalvideosize
1570        write( "Video will fit onto DVD. %s MBytes of space remaining on recordable DVD." % dvdrsize)
1571
1572
1573def createDVDAuthorXML(screensize, numberofitems):
1574    """Creates the xml file for dvdauthor to use the MythBurn menus."""
1575
1576    #Get the main menu node (we must only have 1)
1577    menunode=themeDOM.getElementsByTagName("menu")
1578    if menunode.length!=1:
1579        fatalError("Cannot find the menu element in the theme file")
1580    menunode=menunode[0]
1581
1582    menuitems=menunode.getElementsByTagName("item")
1583    #Total number of video items on a single menu page (no less than 1!)
1584    itemsperpage = menuitems.length
1585    write( "Menu items per page %s" % itemsperpage)
1586
1587    if wantChapterMenu:
1588        #Get the chapter menu node (we must only have 1)
1589        submenunode=themeDOM.getElementsByTagName("submenu")
1590        if submenunode.length!=1:
1591            fatalError("Cannot find the submenu element in the theme file")
1592
1593        submenunode=submenunode[0]
1594
1595        chapteritems=submenunode.getElementsByTagName("chapter")
1596        #Total number of video items on a single menu page (no less than 1!)
1597        chapters = chapteritems.length
1598        write( "Chapters per recording %s" % chapters)
1599
1600        del chapteritems
1601        del submenunode
1602
1603    #Page number counter
1604    page=1
1605
1606    #Item counter to indicate current video item
1607    itemnum=1
1608
1609    write( "Creating DVD XML file for dvd author")
1610
1611    dvddom = xml.dom.minidom.parseString(
1612                '''<dvdauthor>
1613                <vmgm>
1614                <menus lang="en">
1615                <pgc entry="title">
1616                </pgc>
1617                </menus>
1618                </vmgm>
1619                </dvdauthor>''')
1620
1621    dvdauthor_element=dvddom.documentElement
1622    menus_element = dvdauthor_element.childNodes[1].childNodes[1]
1623
1624    dvdauthor_element.insertBefore( dvddom.createComment("""
1625    DVD Variables
1626    g0=not used
1627    g1=not used
1628    g2=title number selected on current menu page (see g4)
1629    g3=1 if intro movie has played
1630    g4=last menu page on display
1631    """), dvdauthor_element.firstChild )
1632    dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
1633
1634    menus_element.appendChild( dvddom.createComment("Title menu used to hold intro movie") )
1635
1636    dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
1637
1638    video = dvddom.createElement("video")
1639    video.setAttribute("format",videomode)
1640
1641    # set aspect ratio
1642    if mainmenuAspectRatio == "4:3":
1643        video.setAttribute("aspect", "4:3")
1644    else:
1645        video.setAttribute("aspect", "16:9")
1646        video.setAttribute("widescreen", "nopanscan")
1647
1648    menus_element.appendChild(video)
1649
1650    pgc=menus_element.childNodes[1]
1651
1652    if wantIntro:
1653        #code to skip over intro if its already played
1654        pre = dvddom.createElement("pre")
1655        pgc.appendChild(pre)
1656        vmgm_pre_node=pre
1657        del pre
1658
1659        node = themeDOM.getElementsByTagName("intro")[0]
1660        introFile = node.attributes["filename"].value
1661
1662        #Pick the correct intro movie based on video format ntsc/pal
1663        vob = dvddom.createElement("vob")
1664        vob.setAttribute("pause","")
1665        vob.setAttribute("file",os.path.join(getIntroPath(), videomode + '_' + introFile))
1666        pgc.appendChild(vob)
1667        del vob
1668
1669        #We use g3 to indicate that the intro has been played at least once
1670        #default g2 to point to first recording
1671        post = dvddom.createElement("post")
1672        post .appendChild(dvddom.createTextNode("{g3=1;g2=1;jump menu 2;}"))
1673        pgc.appendChild(post)
1674        del post
1675
1676    while itemnum <= numberofitems:
1677        write( "Menu page %s" % page)
1678
1679        #For each menu page we need to create a new PGC structure
1680        menupgc = dvddom.createElement("pgc")
1681        menus_element.appendChild(menupgc)
1682        menupgc.setAttribute("pause","inf")
1683
1684        menupgc.appendChild( dvddom.createComment("Menu Page %s" % page) )
1685
1686        #Make sure the button last highlighted is selected
1687        #g4 holds the menu page last displayed
1688        pre = dvddom.createElement("pre")
1689        pre.appendChild(dvddom.createTextNode("{button=g2*1024;g4=%s;}" % page))
1690        menupgc.appendChild(pre)   
1691
1692        vob = dvddom.createElement("vob")
1693        vob.setAttribute("file",os.path.join(getTempPath(),"menu-%s.mpg" % page))
1694        menupgc.appendChild(vob)   
1695
1696        #Loop menu forever
1697        post = dvddom.createElement("post")
1698        post.appendChild(dvddom.createTextNode("jump cell 1;"))
1699        menupgc.appendChild(post)
1700
1701        #Default settings for this page
1702
1703        #Number of video items on this menu page
1704        itemsonthispage=0
1705
1706        #Loop through all the items on this menu page
1707        while itemnum <= numberofitems and itemsonthispage < itemsperpage:
1708            menuitem=menuitems[ itemsonthispage ]
1709
1710            itemsonthispage+=1
1711
1712            #Get the XML containing information about this item
1713            infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(itemnum),"info.xml") )
1714            #Error out if its the wrong XML
1715            if infoDOM.documentElement.tagName != "fileinfo":
1716                fatalError("The info.xml file (%s) doesn't look right" % os.path.join(getItemTempPath(itemnum),"info.xml"))
1717
1718            #write( themedom.toprettyxml())
1719
1720            #Add this recording to this page's menu...
1721            button=dvddom.createElement("button")
1722            button.setAttribute("name","%s" % itemnum)
1723            button.appendChild(dvddom.createTextNode("{g2=" + "%s" % itemsonthispage + "; jump title %s;}" % itemnum))
1724            menupgc.appendChild(button)
1725            del button
1726
1727            #Create a TITLESET for each item
1728            titleset = dvddom.createElement("titleset")
1729            dvdauthor_element.appendChild(titleset)
1730
1731            #Comment XML file with title of video
1732            titleset.appendChild( dvddom.createComment( getText( infoDOM.getElementsByTagName("title")[0]) ) )
1733
1734            menus= dvddom.createElement("menus")
1735            titleset.appendChild(menus)
1736
1737            video = dvddom.createElement("video")
1738            video.setAttribute("format",videomode)
1739
1740            # set the right aspect ratio
1741            if chaptermenuAspectRatio == "4:3":
1742                video.setAttribute("aspect", "4:3")
1743            elif chaptermenuAspectRatio == "16:9":
1744                video.setAttribute("aspect", "16:9")
1745                video.setAttribute("widescreen", "nopanscan")
1746            else:
1747                # use same aspect ratio as the video
1748                if getAspectRatioOfVideo(itemnum) > 1.4:
1749                    video.setAttribute("aspect", "16:9")
1750                    video.setAttribute("widescreen", "nopanscan")
1751                else:
1752                    video.setAttribute("aspect", "4:3")
1753
1754            menus.appendChild(video)
1755
1756            if wantChapterMenu:
1757                mymenupgc = dvddom.createElement("pgc")
1758                menus.appendChild(mymenupgc)
1759                mymenupgc.setAttribute("pause","inf")
1760
1761                pre = dvddom.createElement("pre")
1762                mymenupgc.appendChild(pre)
1763                if wantDetailsPage:
1764                    pre.appendChild(dvddom.createTextNode("{button=s7 - 1 * 1024;}"))
1765                else:
1766                    pre.appendChild(dvddom.createTextNode("{button=s7 * 1024;}"))
1767
1768                vob = dvddom.createElement("vob")
1769                vob.setAttribute("file",os.path.join(getTempPath(),"chaptermenu-%s.mpg" % itemnum))
1770                mymenupgc.appendChild(vob)   
1771
1772                #Loop menu forever
1773                post = dvddom.createElement("post")
1774                post.appendChild(dvddom.createTextNode("jump cell 1;"))
1775                mymenupgc.appendChild(post)
1776
1777                x=1
1778                while x<=chapters:
1779                    #Add this recording to this page's menu...
1780                    button=dvddom.createElement("button")
1781                    button.setAttribute("name","%s" % x)
1782                    if wantDetailsPage:
1783                        button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, x + 1)))
1784                    else:
1785                        button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, x)))
1786
1787                    mymenupgc.appendChild(button)
1788                    del button
1789                    x+=1
1790
1791            titles = dvddom.createElement("titles")
1792            titleset.appendChild(titles)
1793
1794            # set the right aspect ratio
1795            title_video = dvddom.createElement("video")
1796            title_video.setAttribute("format",videomode)
1797
1798            if chaptermenuAspectRatio == "4:3":
1799                title_video.setAttribute("aspect", "4:3")
1800            elif chaptermenuAspectRatio == "16:9":
1801                title_video.setAttribute("aspect", "16:9")
1802                title_video.setAttribute("widescreen", "nopanscan")
1803            else:
1804                # use same aspect ratio as the video
1805                if getAspectRatioOfVideo(itemnum) > 1.4:
1806                    title_video.setAttribute("aspect", "16:9")
1807                    title_video.setAttribute("widescreen", "nopanscan")
1808                else:
1809                    title_video.setAttribute("aspect", "4:3")
1810
1811            titles.appendChild(title_video)
1812
1813            pgc = dvddom.createElement("pgc")
1814            titles.appendChild(pgc)
1815            #pgc.setAttribute("pause","inf")
1816
1817            if wantDetailsPage:
1818                #add the detail page intro for this item
1819                vob = dvddom.createElement("vob")
1820                vob.setAttribute("file",os.path.join(getTempPath(),"details-%s.mpg" % itemnum))
1821                pgc.appendChild(vob)
1822
1823            vob = dvddom.createElement("vob")
1824            if wantChapterMenu:
1825                vob.setAttribute("chapters",createVideoChapters(itemnum,chapters,getLengthOfVideo(itemnum),False) )
1826            else:
1827                vob.setAttribute("chapters", createVideoChaptersFixedLength(chapterLength, getLengthOfVideo(itemnum)))
1828
1829            vob.setAttribute("file",os.path.join(getItemTempPath(itemnum),"final.mpg"))
1830            pgc.appendChild(vob)
1831
1832            post = dvddom.createElement("post")
1833            post.appendChild(dvddom.createTextNode("call vmgm menu %s;" % (page + 1)))
1834            pgc.appendChild(post)
1835
1836            #Quick variable tidy up (not really required under Python)
1837            del titleset
1838            del titles
1839            del menus
1840            del video
1841            del pgc
1842            del vob
1843            del post
1844
1845            #Loop through all the nodes inside this menu item and pick previous / next buttons
1846            for node in menuitem.childNodes:
1847
1848                if node.nodeName=="previous":
1849                    if page>1:
1850                        button=dvddom.createElement("button")
1851                        button.setAttribute("name","previous")
1852                        button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % page ))
1853                        menupgc.appendChild(button)
1854                        del button
1855
1856
1857                elif node.nodeName=="next":
1858                    if itemnum < numberofitems:
1859                        button=dvddom.createElement("button")
1860                        button.setAttribute("name","next")
1861                        button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % (page + 2)))
1862                        menupgc.appendChild(button)
1863                        del button
1864
1865            #On to the next item
1866            itemnum+=1
1867
1868        #Move on to the next page
1869        page+=1
1870
1871    if wantIntro:
1872        #Menu creation is finished so we know how many pages were created
1873        #add to to jump to the correct one automatically
1874        dvdcode="if (g3 eq 1) {"
1875        while (page>1):
1876            page-=1;
1877            dvdcode+="if (g4 eq %s) " % page
1878            dvdcode+="jump menu %s;" % (page + 1)
1879            if (page>1):
1880                dvdcode+=" else "
1881        dvdcode+="}"       
1882        vmgm_pre_node.appendChild(dvddom.createTextNode(dvdcode))
1883
1884    #write(dvddom.toprettyxml())
1885    #Save xml to file
1886    WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
1887
1888    #Destroy the DOM and free memory
1889    dvddom.unlink()   
1890
1891def createDVDAuthorXMLNoMainMenu(screensize, numberofitems):
1892    """Creates the xml file for dvdauthor to use the MythBurn menus."""
1893
1894    # creates a simple DVD with only a chapter menus shown before each video
1895    # can contain an intro movie and each title can have a details page
1896    # displayed before each title
1897
1898    write( "Creating DVD XML file for dvd author (No Main Menu)")
1899    #FIXME:
1900    assert False
1901
1902def createDVDAuthorXMLNoMenus(screensize, numberofitems):
1903    """Creates the xml file for dvdauthor containing no menus."""
1904
1905    # creates a simple DVD with no menus that chains the videos one after the other
1906    # can contain an intro movie and each title can have a details page
1907    # displayed before each title
1908
1909    write( "Creating DVD XML file for dvd author (No Menus)")
1910
1911    dvddom = xml.dom.minidom.parseString(
1912                '''
1913                <dvdauthor>
1914                    <vmgm>
1915                    </vmgm>
1916                </dvdauthor>''')
1917
1918    dvdauthor_element = dvddom.documentElement
1919    titleset = dvddom.createElement("titleset")
1920    titles = dvddom.createElement("titles")
1921    titleset.appendChild(titles)
1922    dvdauthor_element.appendChild(titleset)
1923
1924    dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
1925    dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
1926
1927    fileCount = 0
1928    itemNum = 1
1929
1930    if wantIntro:
1931        node = themeDOM.getElementsByTagName("intro")[0]
1932        introFile = node.attributes["filename"].value
1933
1934        titles.appendChild(dvddom.createComment("Intro movie"))
1935        pgc = dvddom.createElement("pgc")
1936        vob = dvddom.createElement("vob")
1937        vob.setAttribute("file",os.path.join(getIntroPath(), videomode + '_' + introFile))
1938        pgc.appendChild(vob)
1939        titles.appendChild(pgc)
1940        post = dvddom.createElement("post")
1941        post .appendChild(dvddom.createTextNode("jump title 2 chapter 1;"))
1942        pgc.appendChild(post)
1943        titles.appendChild(pgc)
1944        fileCount +=1
1945        del pgc
1946        del vob
1947        del post
1948
1949
1950    while itemNum <= numberofitems:
1951        write( "Adding item %s" % itemNum)
1952
1953        pgc = dvddom.createElement("pgc")
1954
1955        if wantDetailsPage:
1956            #add the detail page intro for this item
1957            vob = dvddom.createElement("vob")
1958            vob.setAttribute("file",os.path.join(getTempPath(),"details-%s.mpg" % itemNum))
1959            pgc.appendChild(vob)
1960            fileCount +=1
1961            del vob
1962
1963        vob = dvddom.createElement("vob")
1964        vob.setAttribute("file", os.path.join(getItemTempPath(itemNum), "final.mpg"))
1965        vob.setAttribute("chapters", createVideoChaptersFixedLength(chapterLength, getLengthOfVideo(itemNum)))
1966        pgc.appendChild(vob)
1967        del vob
1968
1969        post = dvddom.createElement("post")
1970        if itemNum == numberofitems:
1971            post.appendChild(dvddom.createTextNode("exit;"))
1972        else:
1973            if wantIntro:
1974                post.appendChild(dvddom.createTextNode("jump title %d chapter 1;" % (itemNum + 2)))
1975            else:
1976                post.appendChild(dvddom.createTextNode("jump title %d chapter 1;" % (itemNum + 1)))
1977
1978        pgc.appendChild(post)
1979        fileCount +=1
1980
1981        titles.appendChild(pgc)
1982        del pgc
1983
1984        itemNum +=1
1985
1986    #Save xml to file
1987    WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
1988
1989    #Destroy the DOM and free memory
1990    dvddom.unlink()
1991
1992def drawThemeItem(page, itemsonthispage, itemnum, menuitem, bgimage, draw,
1993                  bgimagemask, drawmask, highlightcolor, spumuxdom, spunode,
1994                  numberofitems, chapternumber, chapterlist):
1995    """Draws text and graphics onto a dvd menu, called by createMenu and createChapterMenu"""
1996    #Get the XML containing information about this item
1997    infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(itemnum),"info.xml") )
1998    #Error out if its the wrong XML
1999    if infoDOM.documentElement.tagName != "fileinfo":
2000        fatalError("The info.xml file (%s) doesn't look right" % os.path.join(getItemTempPath(itemnum),"info.xml"))
2001
2002    #boundarybox holds the max and min dimensions for this item so we can auto build a menu highlight box
2003    boundarybox=9999,9999,0,0
2004    wantHighlightBox = True
2005
2006    #Loop through all the nodes inside this menu item
2007    for node in menuitem.childNodes:
2008
2009        #Process each type of item to add it onto the background image
2010        if node.nodeName=="graphic":
2011            #Overlay graphic image onto background
2012            imagefilename = expandItemText(infoDOM,node.attributes["filename"].value, itemnum, page, itemsonthispage, chapternumber, chapterlist)
2013
2014            if doesFileExist(imagefilename):
2015                picture = Image.open(imagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2016                picture = picture.convert("RGBA")
2017                bgimage.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2018                del picture
2019                write( "Added image %s" % imagefilename)
2020
2021                boundarybox=checkBoundaryBox(boundarybox, node)
2022            else:
2023                write( "Image file does not exist '%s'" % imagefilename)
2024
2025        elif node.nodeName=="text":
2026            #Apply some text to the background, including wordwrap if required.
2027            text=expandItemText(infoDOM,node.attributes["value"].value, itemnum, page, itemsonthispage,chapternumber,chapterlist)
2028            if text>"":
2029                paintText( draw,
2030                           getScaledAttribute(node, "x"),
2031                           getScaledAttribute(node, "y"),
2032                           getScaledAttribute(node, "w"),
2033                           getScaledAttribute(node, "h"),
2034                           text,
2035                           themeFonts[int(node.attributes["font"].value)],
2036                           node.attributes["colour"].value,
2037                           node.attributes["align"].value )
2038                boundarybox=checkBoundaryBox(boundarybox, node)
2039            del text
2040
2041        elif node.nodeName=="previous":
2042            if page>1:
2043                #Overlay previous graphic button onto background
2044                imagefilename = getThemeFile(themeName, node.attributes["filename"].value)
2045                if not doesFileExist(imagefilename):
2046                    fatalError("Cannot find image for previous button (%s)." % imagefilename)
2047                maskimagefilename = getThemeFile(themeName, node.attributes["mask"].value)
2048                if not doesFileExist(maskimagefilename):
2049                    fatalError("Cannot find mask image for previous button (%s)." % maskimagefilename)
2050
2051                picture=Image.open(imagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2052                picture=picture.convert("RGBA")
2053                bgimage.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2054                del picture
2055                write( "Added previous button image %s" % imagefilename)
2056
2057                picture=Image.open(maskimagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2058                picture=picture.convert("RGBA")
2059                bgimagemask.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2060                del picture
2061                write( "Added previous button mask image %s" % imagefilename)
2062
2063                button = spumuxdom.createElement("button")
2064                button.setAttribute("name","previous")
2065                button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
2066                button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
2067                button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") + getScaledAttribute(node, "w")))
2068                button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") + getScaledAttribute(node, "h")))
2069                spunode.appendChild(button)
2070
2071
2072        elif node.nodeName=="next":
2073            if itemnum < numberofitems:
2074                #Overlay next graphic button onto background
2075                imagefilename = getThemeFile(themeName, node.attributes["filename"].value)
2076                if not doesFileExist(imagefilename):
2077                    fatalError("Cannot find image for next button (%s)." % imagefilename)
2078                maskimagefilename = getThemeFile(themeName, node.attributes["mask"].value)
2079                if not doesFileExist(maskimagefilename):
2080                    fatalError("Cannot find mask image for next button (%s)." % maskimagefilename)
2081
2082                picture = Image.open(imagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2083                picture = picture.convert("RGBA")
2084                bgimage.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2085                del picture
2086                write( "Added next button image %s " % imagefilename)
2087
2088                picture=Image.open(maskimagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2089                picture=picture.convert("RGBA")
2090                bgimagemask.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2091                del picture
2092                write( "Added next button mask image %s" % imagefilename)
2093
2094                button = spumuxdom.createElement("button")
2095                button.setAttribute("name","next")
2096                button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
2097                button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
2098                button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") + getScaledAttribute(node, "w")))
2099                button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") + getScaledAttribute(node, "h")))
2100                spunode.appendChild(button)
2101
2102        elif node.nodeName=="button":
2103            wantHighlightBox = False
2104
2105            #Overlay item graphic/text button onto background
2106            imagefilename = getThemeFile(themeName, node.attributes["filename"].value)
2107            if not doesFileExist(imagefilename):
2108                    fatalError("Cannot find image for menu button (%s)." % imagefilename)
2109            maskimagefilename = getThemeFile(themeName, node.attributes["mask"].value)
2110            if not doesFileExist(maskimagefilename):
2111                    fatalError("Cannot find mask image for menu button (%s)." % maskimagefilename)
2112
2113            picture=Image.open(imagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2114            picture=picture.convert("RGBA")
2115            bgimage.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")), picture)
2116            del picture
2117
2118            # if we have some text paint that over the image
2119            textnode = node.getElementsByTagName("textnormal")
2120            if textnode.length > 0:
2121                textnode = textnode[0]
2122                text=expandItemText(infoDOM,textnode.attributes["value"].value, itemnum, page, itemsonthispage,chapternumber,chapterlist)
2123                if text>"":
2124                    paintText( draw,
2125                           getScaledAttribute(textnode, "x"),
2126                           getScaledAttribute(textnode, "y"),
2127                           getScaledAttribute(textnode, "w"),
2128                           getScaledAttribute(textnode, "h"),
2129                           text,
2130                           themeFonts[int(textnode.attributes["font"].value)],
2131                           textnode.attributes["colour"].value,
2132                           textnode.attributes["align"].value )
2133                boundarybox=checkBoundaryBox(boundarybox, node)
2134                del text
2135
2136            write( "Added button image %s" % imagefilename)
2137
2138            picture=Image.open(maskimagefilename,"r").resize((getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
2139            picture=picture.convert("RGBA")
2140            bgimagemask.paste(picture, (getScaledAttribute(node, "x"), getScaledAttribute(node, "y")),picture)
2141            #del picture
2142
2143            # if we have some text paint that over the image
2144            textnode = node.getElementsByTagName("textselected")
2145            if textnode.length > 0:
2146                textnode = textnode[0]
2147                text=expandItemText(infoDOM,textnode.attributes["value"].value, itemnum, page, itemsonthispage,chapternumber,chapterlist)
2148                textImage=Image.new("RGBA",picture.size)
2149                textDraw=ImageDraw.Draw(textImage)
2150
2151                if text>"":
2152                    paintText(textDraw,
2153                           getScaledAttribute(node, "x") - getScaledAttribute(textnode, "x"),
2154                           getScaledAttribute(node, "y") - getScaledAttribute(textnode, "y"),
2155                           getScaledAttribute(textnode, "w"),
2156                           getScaledAttribute(textnode, "h"),
2157                           text,
2158                           themeFonts[int(textnode.attributes["font"].value)],
2159                           "white",
2160                           textnode.attributes["align"].value )
2161                #convert the RGB image to a 1 bit image
2162                (width, height) = textImage.size
2163                for y in range(height):
2164                    for x in range(width):
2165                        if textImage.getpixel((x,y)) < (100, 100, 100, 255):
2166                            textImage.putpixel((x,y), (0, 0, 0, 0))
2167                        else:
2168                            textImage.putpixel((x,y), (255, 255, 255, 255))
2169
2170                bgimagemask.paste(textnode.attributes["colour"].value,
2171                            (getScaledAttribute(textnode, "x"), getScaledAttribute(textnode, "y")),
2172                            textImage)
2173                boundarybox=checkBoundaryBox(boundarybox, node)
2174
2175                del text, textImage, textDraw
2176            del picture
2177
2178        elif node.nodeName=="#text" or node.nodeName=="#comment":
2179            #Do nothing
2180            assert True
2181        else:
2182            write( "Dont know how to process %s" % node.nodeName)
2183
2184    if drawmask == None:
2185        return
2186
2187    #Draw the mask for this item
2188
2189    if wantHighlightBox == True:
2190        #Make the boundary box bigger than the content to avoid over write(ing (2 pixels)
2191        boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
2192        #draw.rectangle(boundarybox,outline="white")
2193        drawmask.rectangle(boundarybox,outline=highlightcolor)
2194
2195        #Draw another line to make the box thicker - PIL does not support linewidth
2196        boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
2197        #draw.rectangle(boundarybox,outline="white")
2198        drawmask.rectangle(boundarybox,outline=highlightcolor)
2199
2200    node = spumuxdom.createElement("button")
2201    #Fiddle this for chapter marks....
2202    if chapternumber>0:
2203        node.setAttribute("name","%s" % chapternumber)
2204    else:
2205        node.setAttribute("name","%s" % itemnum)
2206    node.setAttribute("x0","%d" % int(boundarybox[0]))
2207    node.setAttribute("y0","%d" % int(boundarybox[1]))
2208    node.setAttribute("x1","%d" % int(boundarybox[2] + 1))
2209    node.setAttribute("y1","%d" % int(boundarybox[3] + 1))
2210    spunode.appendChild(node)   
2211
2212def createMenu(screensize, screendpi, numberofitems):
2213    """Creates all the necessary menu images and files for the MythBurn menus."""
2214
2215    #Get the main menu node (we must only have 1)
2216    menunode=themeDOM.getElementsByTagName("menu")
2217    if menunode.length!=1:
2218        fatalError("Cannot find menu element in theme file")
2219    menunode=menunode[0]
2220
2221    menuitems=menunode.getElementsByTagName("item")
2222    #Total number of video items on a single menu page (no less than 1!)
2223    itemsperpage = menuitems.length
2224    write( "Menu items per page %s" % itemsperpage)
2225
2226    #Get background image filename
2227    backgroundfilename = menunode.attributes["background"].value
2228    if backgroundfilename=="":
2229        fatalError("Background image is not set in theme file")
2230
2231    backgroundfilename = getThemeFile(themeName,backgroundfilename)
2232    write( "Background image file is %s" % backgroundfilename)
2233    if not doesFileExist(backgroundfilename):
2234        fatalError("Background image not found (%s)" % backgroundfilename)
2235
2236    #Get highlight color
2237    highlightcolor = "red"
2238    if menunode.hasAttribute("highlightcolor"):
2239        highlightcolor = menunode.attributes["highlightcolor"].value
2240
2241    #Get menu music
2242    menumusic = "menumusic.mp2"
2243    if menunode.hasAttribute("music"):
2244        menumusic = menunode.attributes["music"].value
2245
2246    #Get menu length
2247    menulength = 15
2248    if menunode.hasAttribute("length"):
2249        menulength = int(menunode.attributes["length"].value)
2250
2251    write("Music is %s, length is %s seconds" % (menumusic, menulength))
2252
2253    #Page number counter
2254    page=1
2255
2256    #Item counter to indicate current video item
2257    itemnum=1
2258
2259    write( "Creating DVD menus")
2260
2261    while itemnum <= numberofitems:
2262        write( "Menu page %s" % page)
2263
2264        #Default settings for this page
2265
2266        #Number of video items on this menu page
2267        itemsonthispage=0
2268
2269        #Load background image
2270        bgimage=Image.open(backgroundfilename,"r").resize(screensize)
2271        draw=ImageDraw.Draw(bgimage)
2272
2273        #Create image to hold button masks (same size as background)
2274        bgimagemask=Image.new("RGBA",bgimage.size)
2275        drawmask=ImageDraw.Draw(bgimagemask)
2276
2277        spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
2278        spunode = spumuxdom.documentElement.firstChild.firstChild
2279
2280        #Loop through all the items on this menu page
2281        while itemnum <= numberofitems and itemsonthispage < itemsperpage:
2282            menuitem=menuitems[ itemsonthispage ]
2283
2284            itemsonthispage+=1
2285
2286            drawThemeItem(page, itemsonthispage,
2287                        itemnum, menuitem, bgimage,
2288                        draw, bgimagemask, drawmask, highlightcolor,
2289                        spumuxdom, spunode, numberofitems, 0,"")
2290
2291            #On to the next item
2292            itemnum+=1
2293
2294        #Save this menu image and its mask
2295        bgimage.save(os.path.join(getTempPath(),"background-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
2296        bgimagemask.save(os.path.join(getTempPath(),"backgroundmask-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
2297
2298## Experimental!
2299##        for i in range(1,750):
2300##            bgimage.save(os.path.join(getTempPath(),"background-%s-%s.ppm" % (page,i)),"PPM",quality=99,optimize=0)
2301
2302        spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
2303        spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
2304
2305        #Release large amounts of memory ASAP !
2306        del draw
2307        del bgimage
2308        del drawmask
2309        del bgimagemask
2310
2311        #write( spumuxdom.toprettyxml())
2312        WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"spumux-%s.xml" % page))
2313
2314        if mainmenuAspectRatio == "4:3":
2315            aspect_ratio = 2
2316        else:
2317            aspect_ratio = 3
2318
2319        write("Encoding Menu Page %s using aspect ratio '%s'" % (page, mainmenuAspectRatio))
2320        encodeMenu(os.path.join(getTempPath(),"background-%s.png" % page),
2321                    os.path.join(getTempPath(),"temp.m2v"),
2322                    getThemeFile(themeName,menumusic),
2323                    menulength,
2324                    os.path.join(getTempPath(),"temp.mpg"),
2325                    os.path.join(getTempPath(),"spumux-%s.xml" % page),
2326                    os.path.join(getTempPath(),"menu-%s.mpg" % page),
2327                    aspect_ratio)
2328
2329        #Tidy up temporary files
2330####        os.remove(os.path.join(getTempPath(),"spumux-%s.xml" % page))
2331####        os.remove(os.path.join(getTempPath(),"background-%s.png" % page))
2332####        os.remove(os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
2333
2334        #Move on to the next page
2335        page+=1
2336
2337def createChapterMenu(screensize, screendpi, numberofitems):
2338    """Creates all the necessary menu images and files for the MythBurn menus."""
2339
2340    #Get the main menu node (we must only have 1)
2341    menunode=themeDOM.getElementsByTagName("submenu")
2342    if menunode.length!=1:
2343        fatalError("Cannot find submenu element in theme file")
2344    menunode=menunode[0]
2345
2346    menuitems=menunode.getElementsByTagName("chapter")
2347    #Total number of video items on a single menu page (no less than 1!)
2348    itemsperpage = menuitems.length
2349    write( "Chapter items per page %s " % itemsperpage)
2350
2351    #Get background image filename
2352    backgroundfilename = menunode.attributes["background"].value
2353    if backgroundfilename=="":
2354        fatalError("Background image is not set in theme file")
2355    backgroundfilename = getThemeFile(themeName,backgroundfilename)
2356    write( "Background image file is %s" % backgroundfilename)
2357    if not doesFileExist(backgroundfilename):
2358        fatalError("Background image not found (%s)" % backgroundfilename)
2359
2360    #Get highlight color
2361    highlightcolor = "red"
2362    if menunode.hasAttribute("highlightcolor"):
2363        highlightcolor = menunode.attributes["highlightcolor"].value
2364
2365    #Get menu music
2366    menumusic = "menumusic.mp2"
2367    if menunode.hasAttribute("music"):
2368        menumusic = menunode.attributes["music"].value
2369
2370    #Get menu length
2371    menulength = 15
2372    if menunode.hasAttribute("length"):
2373        menulength = int(menunode.attributes["length"].value)
2374
2375    write("Music is %s, length is %s seconds" % (menumusic, menulength))
2376
2377    #Page number counter
2378    page=1
2379
2380    write( "Creating DVD sub-menus")
2381
2382    while page <= numberofitems:
2383        write( "Sub-menu %s " % page)
2384
2385        #Default settings for this page
2386
2387        #Load background image
2388        bgimage=Image.open(backgroundfilename,"r").resize(screensize)
2389        draw=ImageDraw.Draw(bgimage)
2390
2391        #Create image to hold button masks (same size as background)
2392        bgimagemask=Image.new("RGBA",bgimage.size)
2393        drawmask=ImageDraw.Draw(bgimagemask)
2394
2395        spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
2396        spunode = spumuxdom.documentElement.firstChild.firstChild
2397
2398        #Extracting the thumbnails for the video takes an incredibly long time
2399        #need to look at a switch to disable this. or not use FFMPEG
2400        chapterlist=createVideoChapters(page,itemsperpage,getLengthOfVideo(page),True)
2401        chapterlist=string.split(chapterlist,",")
2402
2403        #Loop through all the items on this menu page
2404        chapter=0
2405        while chapter < itemsperpage:  # and itemsonthispage < itemsperpage:
2406            menuitem=menuitems[ chapter ]
2407            chapter+=1
2408
2409            drawThemeItem(page, itemsperpage, page, menuitem,
2410                        bgimage, draw,
2411                        bgimagemask, drawmask, highlightcolor,
2412                        spumuxdom, spunode,
2413                        999, chapter, chapterlist)
2414
2415        #Save this menu image and its mask
2416        bgimage.save(os.path.join(getTempPath(),"chaptermenu-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
2417
2418        bgimagemask.save(os.path.join(getTempPath(),"chaptermenumask-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
2419
2420        spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
2421        spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
2422
2423        #Release large amounts of memory ASAP !
2424        del draw
2425        del bgimage
2426        del drawmask
2427        del bgimagemask
2428
2429        #write( spumuxdom.toprettyxml())
2430        WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"chapterspumux-%s.xml" % page))
2431
2432        if chaptermenuAspectRatio == "4:3":
2433            aspect_ratio = '2'
2434        elif chaptermenuAspectRatio == "16:9":
2435            aspect_ratio = '3'
2436        else:
2437            if getAspectRatioOfVideo(page) > 1.4:
2438                aspect_ratio = '3'
2439            else:
2440                aspect_ratio = '2'
2441
2442        write("Encoding Chapter Menu Page %s using aspect ratio '%s'" % (page, chaptermenuAspectRatio))
2443        encodeMenu(os.path.join(getTempPath(),"chaptermenu-%s.png" % page),
2444                    os.path.join(getTempPath(),"temp.m2v"),
2445                    getThemeFile(themeName,menumusic),
2446                    menulength,
2447                    os.path.join(getTempPath(),"temp.mpg"),
2448                    os.path.join(getTempPath(),"chapterspumux-%s.xml" % page),
2449                    os.path.join(getTempPath(),"chaptermenu-%s.mpg" % page),
2450                    aspect_ratio)
2451
2452        #Tidy up
2453####        os.remove(os.path.join(getTempPath(),"chaptermenu-%s.png" % page))
2454####        os.remove(os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
2455####        os.remove(os.path.join(getTempPath(),"chapterspumux-%s.xml" % page))
2456
2457        #Move on to the next page
2458        page+=1
2459
2460def createDetailsPage(screensize, screendpi, numberofitems):
2461    """Creates all the necessary images and files for the details page."""
2462
2463    write( "Creating details pages")
2464
2465    #Get the detailspage node (we must only have 1)
2466    detailnode=themeDOM.getElementsByTagName("detailspage")
2467    if detailnode.length!=1:
2468        fatalError("Cannot find detailspage element in theme file")
2469    detailnode=detailnode[0]
2470
2471    #Get background image filename
2472    backgroundfilename = detailnode.attributes["background"].value
2473    if backgroundfilename=="":
2474        fatalError("Background image is not set in theme file")
2475    backgroundfilename = getThemeFile(themeName,backgroundfilename)
2476    write( "Background image file is %s" % backgroundfilename)
2477    if not doesFileExist(backgroundfilename):
2478        fatalError("Background image not found (%s)" % backgroundfilename)
2479
2480    #Get menu music
2481    menumusic = "menumusic.mp2"
2482    if detailnode.hasAttribute("music"):
2483        menumusic = detailnode.attributes["music"].value
2484
2485    #Get menu length
2486    menulength = 15
2487    if detailnode.hasAttribute("length"):
2488        menulength = int(detailnode.attributes["length"].value)
2489
2490    write("Music is %s, length is %s seconds" % (menumusic, menulength))
2491
2492    #Item counter to indicate current video item
2493    itemnum=1
2494
2495    while itemnum <= numberofitems:
2496        write( "Creating details page for %s" % itemnum)
2497
2498        #Load background image
2499        bgimage=Image.open(backgroundfilename,"r").resize(screensize)
2500        draw=ImageDraw.Draw(bgimage)
2501
2502        spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
2503        spunode = spumuxdom.documentElement.firstChild.firstChild
2504
2505        drawThemeItem(0, 0, itemnum, detailnode, bgimage, draw, None, None,
2506                      "", spumuxdom, spunode, numberofitems, 0, "")
2507
2508        #Save this details image
2509        bgimage.save(os.path.join(getTempPath(),"details-%s.png" % itemnum),"PNG",quality=99,optimize=0,dpi=screendpi)
2510
2511        #Release large amounts of memory ASAP !
2512        del draw
2513        del bgimage
2514
2515        # always use the same aspect ratio as the video
2516        aspect_ratio='2'
2517        if getAspectRatioOfVideo(itemnum) > 1.4:
2518            aspect_ratio='3'
2519
2520        #write( spumuxdom.toprettyxml())
2521        WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"detailsspumux-%s.xml" % itemnum))
2522
2523        write("Encoding Details Page %s" % itemnum)
2524        encodeMenu(os.path.join(getTempPath(),"details-%s.png" % itemnum),
2525                    os.path.join(getTempPath(),"temp.m2v"),
2526                    getThemeFile(themeName,menumusic),
2527                    menulength,
2528                    os.path.join(getTempPath(),"temp.mpg"),
2529                    "",
2530                    os.path.join(getTempPath(),"details-%s.mpg" % itemnum),
2531                    aspect_ratio)
2532
2533        #On to the next item
2534        itemnum+=1
2535
2536def isMediaAVIFile(file):
2537    fh = open(file, 'rb')
2538    Magic = fh.read(4)
2539    fh.close()
2540    return Magic=="RIFF"
2541
2542def processAudio(folder):
2543    """encode audio to ac3 for better compression and compatability with NTSC players"""
2544
2545    # process track 1
2546    if not encodetoac3 and doesFileExist(os.path.join(folder,'stream0.mp2')):
2547        #don't re-encode to ac3 if the user doesn't want it
2548        os.rename(os.path.join(folder,'stream0.mp2'), os.path.join(folder,'stream0.ac3'))
2549    elif doesFileExist(os.path.join(folder,'stream0.mp2'))==True:
2550        write( "Audio track 1 is in mp2 format - re-encoding to ac3")
2551        encodeAudio("ac3",os.path.join(folder,'stream0.mp2'), os.path.join(folder,'stream0.ac3'),True)
2552    elif doesFileExist(os.path.join(folder,'stream0.mpa'))==True:
2553        write( "Audio track 1 is in mpa format - re-encoding to ac3")
2554        encodeAudio("ac3",os.path.join(folder,'stream0.mpa'), os.path.join(folder,'stream0.ac3'),True)
2555    elif doesFileExist(os.path.join(folder,'stream0.ac3'))==True:
2556        write( "Audio is already in ac3 format")
2557    elif doesFileExist(os.path.join(folder,'stream0.ac3'))==True:
2558        write( "Audio is already in ac3 format")
2559    else:
2560        fatalError("Track 1 - Unknown audio format or de-multiplex failed!")
2561
2562    # process track 2
2563    if not encodetoac3 and doesFileExist(os.path.join(folder,'stream1.mp2')):
2564        #don't re-encode to ac3 if the user doesn't want it
2565        os.rename(os.path.join(folder,'stream1.mp2'), os.path.join(folder,'stream1.ac3'))
2566    elif doesFileExist(os.path.join(folder,'stream1.mp2'))==True:
2567        write( "Audio track 2 is in mp2 format - re-encoding to ac3")
2568        encodeAudio("ac3",os.path.join(folder,'stream1.mp2'), os.path.join(folder,'stream1.ac3'),True)
2569    elif doesFileExist(os.path.join(folder,'stream1.mpa'))==True:
2570        write( "Audio track 2 is in mpa format - re-encoding to ac3")
2571        encodeAudio("ac3",os.path.join(folder,'stream1.mpa'), os.path.join(folder,'stream1.ac3'),True)
2572    elif doesFileExist(os.path.join(folder,'stream1.ac3'))==True:
2573        write( "Audio is already in ac3 format")
2574    elif doesFileExist(os.path.join(folder,'stream1.ac3'))==True:
2575        write( "Audio is already in ac3 format")
2576
2577
2578# tuple index constants
2579VIDEO_INDEX = 0
2580VIDEO_CODEC = 1
2581VIDEO_ID    = 2
2582
2583AUDIO_INDEX = 0
2584AUDIO_CODEC = 1
2585AUDIO_ID    = 2
2586AUDIO_LANG  = 3
2587
2588def selectStreams(folder):
2589    """Choose the streams we want from the source file"""
2590
2591    video    = (-1, 'N/A', -1)         # index, codec, ID
2592    audio1   = (-1, 'N/A', -1, 'N/A')  # index, codec, ID, lang
2593    audio2   = (-1, 'N/A', -1, 'N/A')
2594
2595    #open the XML containing information about this file
2596    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
2597    #error out if its the wrong XML
2598    if infoDOM.documentElement.tagName != "file":
2599        fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
2600
2601
2602    #get video ID, CODEC
2603    nodes = infoDOM.getElementsByTagName("video")
2604    if nodes.length == 0:
2605        write("Didn't find any video elements in stream info file.!!!")
2606        write("");
2607        sys.exit(1)
2608    if nodes.length > 1:
2609        write("Found more than one video element in stream info file.!!!")
2610    node = nodes[0]
2611    video = (int(node.attributes["ffmpegindex"].value), node.attributes["codec"].value, int(node.attributes["id"].value))
2612
2613    #get audioID's - we choose the best 2 audio streams using this algorithm
2614    # 1. if there is one or more stream(s) using the 1st preferred language we use that
2615    # 2. if there is one or more stream(s) using the 2nd preferred language we use that
2616    # 3. if we still haven't found a stream we use the stream with the lowest PID
2617    # 4. we prefer ac3 over mp2
2618    # 5. if there are more that one stream with the chosen language we use the one with the lowest PID
2619
2620    write("Preferred audio languages %s and %s" % (preferredlang1, preferredlang2))
2621
2622    nodes = infoDOM.getElementsByTagName("audio")
2623
2624    if nodes.length == 0:
2625        write("Didn't find any audio elements in stream info file.!!!")
2626        write("");
2627        sys.exit(1)
2628
2629    found = False
2630    # first try to find a stream with ac3 and preferred language 1
2631    for node in nodes:
2632        index = int(node.attributes["ffmpegindex"].value)
2633        lang = node.attributes["language"].value
2634        format = string.upper(node.attributes["codec"].value)
2635        pid = int(node.attributes["id"].value)
2636        if lang == preferredlang1 and format == "AC3":
2637            if found:
2638                if pid < audio1[AUDIO_ID]:
2639                    audio1 = (index, format, pid, lang)
2640            else:
2641                audio1 = (index, format, pid, lang)
2642            found = True
2643
2644    # second try to find a stream with mp2 and preferred language 1
2645    if not found:
2646        for node in nodes:
2647            index = int(node.attributes["ffmpegindex"].value)
2648            lang = node.attributes["language"].value
2649            format = string.upper(node.attributes["codec"].value)
2650            pid = int(node.attributes["id"].value)
2651            if lang == preferredlang1 and format == "MP2":
2652                if found:
2653                    if pid < audio1[AUDIO_ID]:
2654                        audio1 = (index, format, pid, lang)
2655                else:
2656                    audio1 = (index, format, pid, lang)
2657                found = True
2658
2659    # finally use the stream with the lowest pid, prefer ac3 over mp2
2660    if not found:
2661        for node in nodes:
2662            index = int(node.attributes["ffmpegindex"].value)
2663            format = string.upper(node.attributes["codec"].value)
2664            pid = int(node.attributes["id"].value)
2665            if not found:
2666                audio1 = (index, format, pid, lang)
2667                found = True
2668            else:
2669                if format == "AC3" and audio1[AUDIO_CODEC] == "MP2":
2670                    audio1 = (index, format, pid, lang)
2671                else:
2672                    if pid < audio1[AUDIO_ID]:
2673                        audio1 = (index, format, pid, lang)
2674
2675    # do we need to find a second audio stream?
2676    if preferredlang1 != preferredlang2 and nodes.length > 1:
2677        found = False
2678        # first try to find a stream with ac3 and preferred language 2
2679        for node in nodes:
2680            index = int(node.attributes["ffmpegindex"].value)
2681            lang = node.attributes["language"].value
2682            format = string.upper(node.attributes["codec"].value)
2683            pid = int(node.attributes["id"].value)
2684            if lang == preferredlang2 and format == "AC3":
2685                if found:
2686                    if pid < audio2[AUDIO_ID]:
2687                        audio2 = (index, format, pid, lang)
2688                else:
2689                    audio2 = (index, format, pid, lang)
2690                found = True
2691
2692        # second try to find a stream with mp2 and preferred language 2
2693        if not found:
2694            for node in nodes:
2695                index = int(node.attributes["ffmpegindex"].value)
2696                lang = node.attributes["language"].value
2697                format = string.upper(node.attributes["codec"].value)
2698                pid = int(node.attributes["id"].value)
2699                if lang == preferredlang2 and format == "MP2":
2700                    if found:
2701                        if pid < audio2[AUDIO_ID]:
2702                            audio2 = (index, format, pid, lang)
2703                    else:
2704                        audio2 = (index, format, pid, lang)
2705                    found = True
2706
2707        # finally use the stream with the lowest pid, prefer ac3 over mp2
2708        if not found:
2709            for node in nodes:
2710                index = int(node.attributes["ffmpegindex"].value)
2711                format = string.upper(node.attributes["codec"].value)
2712                pid = int(node.attributes["id"].value)
2713                if not found:
2714                    # make sure we don't choose the same stream as audio1
2715                    if pid != audio1[AUDIO_ID]:
2716                        audio2 = (index, format, pid, lang)
2717                        found = True
2718                else:
2719                    if format == "AC3" and audio2[AUDIO_CODEC] == "MP2" and pid != audio1[AUDIO_ID]:
2720                        audio2 = (index, format, pid, lang)
2721                    else:
2722                        if pid < audio2[AUDIO_ID] and pid != audio1[AUDIO_ID]:
2723                            audio2 = (index, format, pid, lang)
2724
2725    write("Video id: 0x%x, Audio1: [%d] 0x%x (%s, %s), Audio2: [%d] - 0x%x (%s, %s)" % \
2726        (video[VIDEO_ID], audio1[AUDIO_INDEX], audio1[AUDIO_ID], audio1[AUDIO_CODEC], audio1[AUDIO_LANG], \
2727         audio2[AUDIO_INDEX], audio2[AUDIO_ID], audio2[AUDIO_CODEC], audio2[AUDIO_LANG]))
2728
2729    return (video, audio1, audio2)
2730
2731def selectAspectRatio(folder):
2732    """figure out what aspect ratio we want from the source file"""
2733
2734    #this should be smarter and look though the file for any AR changes
2735    #at the moment it just uses the AR found at the start of the file
2736
2737    #open the XML containing information about this file
2738    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
2739    #error out if its the wrong XML
2740    if infoDOM.documentElement.tagName != "file":
2741        fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
2742
2743
2744    #get aspect ratio
2745    nodes = infoDOM.getElementsByTagName("video")
2746    if nodes.length == 0:
2747        write("Didn't find any video elements in stream info file.!!!")
2748        write("");
2749        sys.exit(1)
2750    if nodes.length > 1:
2751        write("Found more than one video element in stream info file.!!!")
2752    node = nodes[0]
2753    try:
2754        ar = float(node.attributes["aspectratio"].value)
2755        if ar > float(4.0/3.0 - 0.01) and ar < float(4.0/3.0 + 0.01):
2756            aspectratio = "4:3"
2757            write("Aspect ratio is 4:3")
2758        elif ar > float(16.0/9.0 - 0.01) and ar < float(16.0/9.0 + 0.01):
2759            aspectratio = "16:9"
2760            write("Aspect ratio is 16:9")
2761        else:
2762            write("Unknown aspect ratio %f - Using 16:9" % ar)
2763            aspectratio = "16:9"
2764    except:
2765        aspectratio = "16:9"
2766
2767    return aspectratio
2768
2769def getVideoCodec(folder):
2770    """Get the video codec from the streaminfo.xml for the file"""
2771
2772    #open the XML containing information about this file
2773    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
2774    #error out if its the wrong XML
2775    if infoDOM.documentElement.tagName != "file":
2776        fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
2777
2778    nodes = infoDOM.getElementsByTagName("video")
2779    if nodes.length == 0:
2780        write("Didn't find any video elements in stream info file!!!")
2781        write("");
2782        sys.exit(1)
2783    if nodes.length > 1:
2784        write("Found more than one video element in stream info file!!!")
2785    node = nodes[0]
2786    return node.attributes["codec"].value
2787
2788def getFileType(folder):
2789    """Get the overall file type from the streaminfo.xml for the file"""
2790
2791    #open the XML containing information about this file
2792    infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
2793    #error out if its the wrong XML
2794    if infoDOM.documentElement.tagName != "file":
2795        fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
2796
2797    nodes = infoDOM.getElementsByTagName("file")
2798    if nodes.length == 0:
2799        write("Didn't find any file elements in stream info file!!!")
2800        write("");
2801        sys.exit(1)
2802    if nodes.length > 1:
2803        write("Found more than one file element in stream info file!!!")
2804    node = nodes[0]
2805
2806    return node.attributes["type"].value
2807
2808def isFileOkayForDVD(file, folder):
2809    """return true if the file is dvd compliant"""
2810
2811    if string.lower(getVideoCodec(folder)) != "mpeg2video":
2812        return False
2813
2814#    if string.lower(getAudioCodec(folder)) != "ac3" and encodeToAC3:
2815#        return False
2816
2817    videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
2818
2819    # has the user elected to re-encode the file
2820    if file.hasAttribute("encodingprofile"):
2821        if file.attributes["encodingprofile"].value != "NONE":
2822            write("File will be re-encoded using profile %s" % file.attributes["encodingprofile"].value)
2823            return False
2824
2825    if not isResolutionOkayForDVD(videosize):
2826        # file does not have a dvd resolution
2827        if file.hasAttribute("encodingprofile"):
2828            if file.attributes["encodingprofile"].value == "NONE":
2829                write("WARNING: File does not have a DVD compliant resolution but "
2830                      "you have selected not to re-encode the file")
2831                return True
2832        else:
2833            return False
2834
2835    return True
2836
2837def processFile(file, folder):
2838    """Process a single video/recording file ready for burning."""
2839
2840    write( "*************************************************************")
2841    write( "Processing file " + file.attributes["filename"].value + " of type " + file.attributes["type"].value)
2842    write( "*************************************************************")
2843
2844    #As part of this routine we need to pre-process the video this MAY mean:
2845    #1. removing commercials/cleaning up mpeg2 stream
2846    #2. encoding to mpeg2 (if its an avi for instance or isn't DVD compatible)
2847    #3. selecting audio track to use and encoding audio from mp2 into ac3
2848    #4. de-multiplexing into video and audio steams)
2849
2850    mediafile=""
2851
2852    if file.hasAttribute("localfilename"):
2853        mediafile=file.attributes["localfilename"].value
2854    elif file.attributes["type"].value=="recording":
2855        mediafile = os.path.join(recordingpath, file.attributes["filename"].value)
2856    elif file.attributes["type"].value=="video":
2857        mediafile=os.path.join(videopath, file.attributes["filename"].value)
2858    elif file.attributes["type"].value=="file":
2859        mediafile=file.attributes["filename"].value
2860    else:
2861        fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
2862
2863    #Get the XML containing information about this item
2864    infoDOM = xml.dom.minidom.parse( os.path.join(folder,"info.xml") )
2865    #Error out if its the wrong XML
2866    if infoDOM.documentElement.tagName != "fileinfo":
2867        fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
2868
2869    #If this is an mpeg2 myth recording and there is a cut list available and the user wants to use it
2870    #run mythtranscode to cut out commercials etc
2871    if file.attributes["type"].value == "recording":
2872        #can only use mythtranscode to cut commercials on mpeg2 files
2873        write("File type is '%s'" % getFileType(folder))
2874        write("Video codec is '%s'" % getVideoCodec(folder))
2875        if string.lower(getVideoCodec(folder)) == "mpeg2video":
2876            if file.attributes["usecutlist"].value == "1" and getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes":
2877                # Run from local file?
2878                if file.hasAttribute("localfilename"):
2879                    localfile = file.attributes["localfilename"].value
2880                else:
2881                    localfile = ""
2882                write("File has a cut list - running mythtrancode to remove unwanted segments")
2883                chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
2884                starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
2885                if runMythtranscode(chanid, starttime, os.path.join(folder,'tmp'), True, localfile):
2886                    mediafile = os.path.join(folder,'tmp')
2887                else:
2888                    write("Failed to run mythtranscode to remove unwanted segments")
2889            else:
2890                #does the user always want to run recordings through mythtranscode?
2891                #may help to fix any errors in the file
2892                if (alwaysRunMythtranscode == True or
2893                        (getFileType(folder) == "mpegts" and isFileOkayForDVD(file, folder))):
2894                    # Run from local file?
2895                    if file.hasAttribute("localfilename"):
2896                        localfile = file.attributes["localfilename"].value
2897                    else:
2898                        localfile = ""
2899                    write("Running mythtranscode --mpeg2 to fix any errors")
2900                    chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
2901                    starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
2902                    if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
2903                        mediafile = os.path.join(folder, 'newfile.mpg')
2904                    else:
2905                        write("Failed to run mythtrancode to fix any errors")
2906    else:
2907        #does the user always want to run mpeg2 files through mythtranscode?
2908        #may help to fix any errors in the file
2909        write("File type is '%s'" % getFileType(folder))
2910        write("Video codec is '%s'" % getVideoCodec(folder))
2911
2912        if (alwaysRunMythtranscode == True and
2913                string.lower(getVideoCodec(folder)) == "mpeg2video" and
2914                isFileOkayForDVD(file, folder)):
2915            if file.hasAttribute("localfilename"):
2916                localfile = file.attributes["localfilename"].value
2917            else:
2918                localfile = file.attributes["filename"].value
2919            write("Running mythtranscode --mpeg2 to fix any errors")
2920            chanid = -1
2921            starttime = -1
2922            if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
2923                mediafile = os.path.join(folder, 'newfile.mpg')
2924            else:
2925                write("Failed to run mythtrancode to fix any errors")
2926
2927    #do we need to re-encode the file to make it DVD compliant?
2928    if not isFileOkayForDVD(file, folder):
2929        if getFileType(folder) == 'nuv':
2930            #file is a nuv file which ffmpeg has problems reading so use mythtranscode to pass
2931            #the video and audio stream to ffmpeg to do the reencode
2932
2933            #we need to re-encode the file, make sure we get the right video/audio streams
2934            #would be good if we could also split the file at the same time
2935            getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
2936
2937            #choose which streams we need
2938            video, audio1, audio2 = selectStreams(folder)
2939
2940            #choose which aspect ratio we should use
2941            aspectratio = selectAspectRatio(folder)
2942
2943            write("Re-encoding audio and video from nuv file")
2944
2945            # Run from local file?
2946            if file.hasAttribute("localfilename"):
2947                mediafile = file.attributes["localfilename"].value
2948
2949            # what encoding profile should we use
2950            if file.hasAttribute("encodingprofile"):
2951                profile = file.attributes["encodingprofile"].value
2952            else:
2953                profile = defaultEncodingProfile
2954
2955            chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
2956            starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
2957            usecutlist = (file.attributes["usecutlist"].value == "1" and
2958                          getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes")
2959
2960            #do the re-encode
2961            encodeNuvToMPEG2(chanid, starttime, os.path.join(folder, "newfile2.mpg"), folder,
2962                             profile, usecutlist)
2963            mediafile = os.path.join(folder, 'newfile2.mpg')
2964        else:
2965            #we need to re-encode the file, make sure we get the right video/audio streams
2966            #would be good if we could also split the file at the same time
2967            getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
2968
2969            #choose which streams we need
2970            video, audio1, audio2 = selectStreams(folder)
2971
2972            #choose which aspect ratio we should use
2973            aspectratio = selectAspectRatio(folder)
2974
2975            write("Re-encoding audio and video")
2976
2977            # Run from local file?
2978            if file.hasAttribute("localfilename"):
2979                mediafile = file.attributes["localfilename"].value
2980
2981            # what encoding profile should we use
2982            if file.hasAttribute("encodingprofile"):
2983                profile = file.attributes["encodingprofile"].value
2984            else:
2985                profile = defaultEncodingProfile
2986
2987            #do the re-encode
2988            encodeVideoToMPEG2(mediafile, os.path.join(folder, "newfile2.mpg"), video,
2989                            audio1, audio2, aspectratio, profile)
2990            mediafile = os.path.join(folder, 'newfile2.mpg')
2991
2992    #remove an intermediate file
2993    if os.path.exists(os.path.join(folder, "newfile1.mpg")):
2994        os.remove(os.path.join(folder,'newfile1.mpg'))
2995
2996    # the file is now DVD compliant split it into video and audio parts
2997
2998    # find out what streams we have available now
2999    getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 1)
3000
3001    # choose which streams we need
3002    video, audio1, audio2 = selectStreams(folder)
3003
3004    # now attempt to split the source file into video and audio parts
3005    write("Splitting MPEG stream into audio and video parts")
3006    deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2)
3007
3008    if os.path.exists(os.path.join(folder, "newfile2.mpg")):
3009        os.remove(os.path.join(folder,'newfile2.mpg'))
3010
3011    # we now have a video stream and one or more audio streams
3012    # check if we need to convert any of the audio streams to ac3
3013    processAudio(folder)
3014
3015    #do a quick sense check before we continue...
3016    assert doesFileExist(os.path.join(folder,'stream.mv2'))
3017    assert doesFileExist(os.path.join(folder,'stream0.ac3'))
3018    #assert doesFileExist(os.path.join(folder,'stream1.ac3'))
3019
3020    extractVideoFrame(os.path.join(folder,"stream.mv2"), os.path.join(folder,"thumbnail.jpg"), 0)
3021
3022    write( "*************************************************************")
3023    write( "Finished processing file " + file.attributes["filename"].value)
3024    write( "*************************************************************")
3025
3026def copyRemote(files,tmpPath):
3027    from shutil import copy
3028
3029    localTmpPath = os.path.join(tmpPath, "localcopy")
3030    # Define remote filesystems
3031    remotefs = ['nfs','smbfs']
3032    remotemounts = []
3033    # What does mount say?
3034    mounts = os.popen('mount')
3035    # Go through each line of mounts output
3036    for line in mounts.readlines():
3037        parts = line.split()
3038        # mount says in this format
3039        device, txt1, mountpoint, txt2, filesystem, options = parts
3040        # only do if really remote
3041        if filesystem in remotefs:
3042            # add remote to list
3043            remotemounts.append(string.split(mountpoint,'/'))
3044            # go through files
3045            for node in files:
3046                # go through list
3047                for mount in remotemounts:
3048                    # Recordings have no path in xml file generated by mytharchive.
3049                    #
3050                    # Maybe better to put real path in xml like file and video have it.
3051                    if node.attributes["type"].value == "recording":
3052                        tmpfile = string.split(os.path.join(recordingpath, node.attributes["filename"].value), '/')
3053                    else:
3054                        tmpfile = string.split(node.attributes["filename"].value, '/')
3055                    filename = tmpfile[len(tmpfile)-1]
3056                    tmpfiledirs=""
3057                    tmpremotedir=""
3058                    # path has to be minimum length of mountpoint
3059                    if len(tmpfile) > len(mount):
3060                        for i in range(len(mount)):
3061                            tmpfiledirs = tmpfiledirs + tmpfile[i] + "/"
3062                        for i in range(len(mount)):
3063                            tmpremotedir = tmpremotedir + mount[i] + "/"
3064                        # Is it like the mount point?
3065                        if tmpfiledirs == tmpremotedir:
3066                            # Write that we copy
3067                            write("Copying file from " +os.path.join(recordingpath, node.attributes["filename"].value))
3068                            write("to " + os.path.join(localTmpPath, filename))
3069                            # Copy file
3070                            if not doesFileExist(os.path.join(localTmpPath, filename)):
3071                                copy(os.path.join(recordingpath, node.attributes["filename"].value),os.path.join(localTmpPath, filename))
3072                            # update node
3073                            node.setAttribute("localfilename", os.path.join(localTmpPath, filename))
3074                            print node.attributes["localfilename"].value
3075    return files
3076
3077def processJob(job):
3078    """Starts processing a MythBurn job, expects XML nodes to be passed as input."""
3079    global wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage
3080    global themeDOM, themeName, themeFonts
3081
3082    media=job.getElementsByTagName("media")
3083
3084    if media.length==1:
3085
3086        themeName=job.attributes["theme"].value
3087
3088        #Check theme exists
3089        if not validateTheme(themeName):
3090            fatalError("Failed to validate theme (%s)" % themeName)
3091        #Get the theme XML
3092        themeDOM = getThemeConfigurationXML(themeName)
3093
3094        #Pre generate all the fonts we need
3095        loadFonts(themeDOM)
3096
3097        #Update the global flags
3098        nodes=themeDOM.getElementsByTagName("intro")
3099        wantIntro = (nodes.length > 0)
3100
3101        nodes=themeDOM.getElementsByTagName("menu")
3102        wantMainMenu = (nodes.length > 0)
3103
3104        nodes=themeDOM.getElementsByTagName("submenu")
3105        wantChapterMenu = (nodes.length > 0)
3106
3107        nodes=themeDOM.getElementsByTagName("detailspage")
3108        wantDetailsPage = (nodes.length > 0)
3109
3110        write( "wantIntro: %d, wantMainMenu: %d, wantChapterMenu:%d, wantDetailsPage: %d" \
3111                % (wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage))
3112
3113        if videomode=="ntsc":
3114            format=dvdNTSC
3115            dpi=dvdNTSCdpi
3116        elif videomode=="pal":
3117            format=dvdPAL
3118            dpi=dvdPALdpi
3119        else:
3120            fatalError("Unknown videomode is set (%s)" % videomode)
3121
3122        write( "Final DVD Video format will be " + videomode)
3123
3124        #Ensure the destination dvd folder is empty
3125        if doesFileExist(os.path.join(getTempPath(),"dvd")):
3126            deleteAllFilesInFolder(os.path.join(getTempPath(),"dvd"))
3127
3128        #Loop through all the files
3129        files=media[0].getElementsByTagName("file")
3130        filecount=0
3131        if files.length > 0:
3132            write( "There are %s files to process" % files.length)
3133
3134            if debug_secondrunthrough==False:
3135                #Delete all the temporary files that currently exist
3136                deleteAllFilesInFolder(getTempPath())
3137
3138            #If User wants to, copy remote files to a tmp dir
3139            if copyremoteFiles==True:
3140                if debug_secondrunthrough==False:
3141                    localCopyFolder=os.path.join(getTempPath(),"localcopy")
3142                    #If it already exists destroy it to remove previous debris
3143                    if os.path.exists(localCopyFolder):
3144                        #Remove all the files first
3145                        deleteAllFilesInFolder(localCopyFolder)
3146                        #Remove the folder
3147                        os.rmdir (localCopyFolder)
3148                    os.makedirs(localCopyFolder)
3149                files=copyRemote(files,getTempPath())
3150
3151            #First pass through the files to be recorded - sense check
3152            #we dont want to find half way through this long process that
3153            #a file does not exist, or is the wrong format!!
3154            for node in files:
3155                filecount+=1
3156
3157                #Generate a temp folder name for this file
3158                folder=getItemTempPath(filecount)
3159
3160                if debug_secondrunthrough==False:
3161                    #If it already exists destroy it to remove previous debris
3162                    if os.path.exists(folder):
3163                        #Remove all the files first
3164                        deleteAllFilesInFolder(folder)
3165                        #Remove the folder
3166                        os.rmdir (folder)
3167                    os.makedirs(folder)
3168                #Do the pre-process work
3169                preProcessFile(node,folder)
3170
3171            if debug_secondrunthrough==False:
3172                #Loop through all the files again but this time do more serious work!
3173                filecount=0
3174                for node in files:
3175                    filecount+=1
3176                    folder=getItemTempPath(filecount)
3177
3178                    #Process this file
3179                    processFile(node,folder)
3180
3181            #We can only create the menus after the videos have been processed
3182            #and the commercials cut out so we get the correct run time length
3183            #for the chapter marks and thumbnails.
3184            #create the DVD menus...
3185            if wantMainMenu:
3186                createMenu(format, dpi, files.length)
3187
3188            #Submenus are visible when you select the chapter menu while the recording is playing
3189            if wantChapterMenu:
3190                createChapterMenu(format, dpi, files.length)
3191
3192            #Details Page are displayed just before playing each recording
3193            if wantDetailsPage:
3194                createDetailsPage(format, dpi, files.length)
3195
3196            #DVD Author file
3197            if not wantMainMenu and not wantChapterMenu:
3198                createDVDAuthorXMLNoMenus(format, files.length)
3199            elif not wantMainMenu:
3200                createDVDAuthorXMLNoMainMenu(format, files.length)
3201            else:
3202                createDVDAuthorXML(format, files.length)
3203
3204            #Check all the files will fit onto a recordable DVD
3205            if mediatype == DVD_DL:
3206                # dual layer
3207                performMPEG2Shrink(files, dvdrsize[1])
3208            else:
3209                #single layer
3210                performMPEG2Shrink(files, dvdrsize[0])
3211
3212            filecount=0
3213            for node in files:
3214                filecount+=1
3215                folder=getItemTempPath(filecount)
3216                #Multiplex this file
3217                #(This also removes non-required audio feeds inside mpeg streams
3218                #(through re-multiplexing) we only take 1 video and 1 or 2 audio streams)
3219                pid=multiplexMPEGStream(os.path.join(folder,'stream.mv2'),
3220                        os.path.join(folder,'stream0.ac3'),
3221                        os.path.join(folder,'stream1.ac3'),
3222                        os.path.join(folder,'final.mpg'))
3223
3224            #Now all the files are completed and ready to be burnt
3225            runDVDAuthor()
3226
3227            #Create the DVD ISO image
3228            if docreateiso == True or mediatype == FILE:
3229                CreateDVDISO()
3230
3231            #Burn the DVD ISO image
3232            if doburn == True and mediatype != FILE:
3233                BurnDVDISO()
3234
3235            #Move the created iso image to the given location
3236            if mediatype == FILE and savefilename != "":
3237                write("Moving ISO image to: %s" % savefilename)
3238                try:
3239                    os.rename(os.path.join(getTempPath(), 'mythburn.iso'), savefilename)
3240                except:
3241                    f1 = open(os.path.join(getTempPath(), 'mythburn.iso'), 'rb')
3242                    f2 = open(savefilename, 'wb')
3243                    data = f1.read(1024 * 1024)
3244                    while data:
3245                        f2.write(data)
3246                        data = f1.read(1024 * 1024)
3247                    f1.close()
3248                    f2.close()
3249                    os.unlink(os.path.join(getTempPath(), 'mythburn.iso'))
3250        else:
3251            write( "Nothing to do! (files)")
3252    else:
3253        write( "Nothing to do! (media)")
3254    return
3255
3256def usage():
3257    write("""
3258    -h/--help               (Show this usage)
3259    -j/--jobfile file       (use file as the job file)
3260    -l/--progresslog file   (log file to output progress messages)
3261
3262    """)
3263
3264#
3265#
3266# The main starting point for mythburn.py
3267#
3268#
3269
3270write( "mythburn.py (%s) starting up..." % VERSION)
3271
3272nice=os.nice(8)
3273write( "Process priority %s" % nice)
3274
3275#Ensure were running at least python 2.3.5
3276if not hasattr(sys, "hexversion") or sys.hexversion < 0x20305F0:
3277    sys.stderr.write("Sorry, your Python is too old. Please upgrade at least to 2.3.5\n")
3278    sys.exit(1)
3279
3280# figure out where this script is located
3281scriptpath = os.path.dirname(sys.argv[0])
3282scriptpath = os.path.abspath(scriptpath)
3283write("script path:" + scriptpath)
3284
3285# figure out where the myth share directory is located
3286sharepath = os.path.split(scriptpath)[0]
3287sharepath = os.path.split(sharepath)[0]
3288write("myth share path:" + sharepath)
3289
3290# process any command line options
3291try:
3292    opts, args = getopt.getopt(sys.argv[1:], "j:hl:", ["jobfile=", "help", "progresslog="])
3293except getopt.GetoptError:
3294    # print usage and exit
3295    usage()
3296    sys.exit(2)
3297
3298for o, a in opts:
3299    if o in ("-h", "--help"):
3300        usage()
3301        sys.exit()
3302    if o in ("-j", "--jobfile"):
3303        jobfile = str(a)
3304        write("passed job file: " + a)
3305    if o in ("-l", "--progresslog"):
3306        progresslog = str(a)
3307        write("passed progress log file: " + a)
3308
3309#if we have been given a progresslog filename to write to open it
3310if progresslog != "":
3311    if os.path.exists(progresslog):
3312        os.remove(progresslog)
3313    progressfile = open(progresslog, 'w')
3314    write( "mythburn.py (%s) starting up..." % VERSION)
3315
3316
3317#Get mysql database parameters
3318getMysqlDBParameters();
3319
3320#if the script is run from the web interface the PATH environment variable does not include
3321#many of the bin locations we need so just append a few likely locations where our required
3322#executables may be
3323if not os.environ['PATH'].endswith(':'):
3324    os.environ['PATH'] += ":"
3325os.environ['PATH'] += "/bin:/sbin:/usr/local/bin:/usr/bin:/opt/bin:" + installPrefix +"/bin:"
3326
3327#Get defaults from MythTV database
3328defaultsettings = getDefaultParametersFromMythTVDB()
3329videopath = defaultsettings.get("VideoStartupDir", None)
3330recordingpath = defaultsettings.get("RecordFilePrefix", None)
3331gallerypath = defaultsettings.get("GalleryDir", None)
3332musicpath = defaultsettings.get("MusicLocation", None)
3333videomode = string.lower(defaultsettings["MythArchiveVideoFormat"])
3334temppath = defaultsettings["MythArchiveTempDir"] + "/work"
3335logpath = defaultsettings["MythArchiveTempDir"] + "/logs"
3336dvddrivepath = defaultsettings["MythArchiveDVDLocation"]
3337dbVersion = defaultsettings["DBSchemaVer"]
3338preferredlang1 = defaultsettings["ISO639Language0"]
3339preferredlang2 = defaultsettings["ISO639Language1"]
3340useFIFO = (defaultsettings["MythArchiveUseFIFO"] == '1')
3341encodetoac3 = (defaultsettings["MythArchiveEncodeToAc3"] == '1')
3342alwaysRunMythtranscode = (defaultsettings["MythArchiveAlwaysUseMythTranscode"] == '1')
3343copyremoteFiles = (defaultsettings["MythArchiveCopyRemoteFiles"] == '1')
3344mainmenuAspectRatio = defaultsettings["MythArchiveMainMenuAR"]
3345chaptermenuAspectRatio = defaultsettings["MythArchiveChapterMenuAR"]
3346
3347# external commands
3348path_mplex = [defaultsettings["MythArchiveMplexCmd"], os.path.split(defaultsettings["MythArchiveMplexCmd"])[1]]
3349path_ffmpeg = [defaultsettings["MythArchiveFfmpegCmd"], os.path.split(defaultsettings["MythArchiveFfmpegCmd"])[1]]
3350path_dvdauthor = [defaultsettings["MythArchiveDvdauthorCmd"], os.path.split(defaultsettings["MythArchiveDvdauthorCmd"])[1]]
3351path_mkisofs = [defaultsettings["MythArchiveMkisofsCmd"], os.path.split(defaultsettings["MythArchiveMkisofsCmd"])[1]]
3352path_growisofs = [defaultsettings["MythArchiveGrowisofsCmd"], os.path.split(defaultsettings["MythArchiveGrowisofsCmd"])[1]]
3353path_tcrequant = [defaultsettings["MythArchiveTcrequantCmd"], os.path.split(defaultsettings["MythArchiveTcrequantCmd"])[1]]
3354path_png2yuv = [defaultsettings["MythArchivePng2yuvCmd"], os.path.split(defaultsettings["MythArchivePng2yuvCmd"])[1]]
3355path_spumux = [defaultsettings["MythArchiveSpumuxCmd"], os.path.split(defaultsettings["MythArchiveSpumuxCmd"])[1]]
3356path_mpeg2enc = [defaultsettings["MythArchiveMpeg2encCmd"], os.path.split(defaultsettings["MythArchiveMpeg2encCmd"])[1]]
3357
3358try:
3359    try:
3360        # create our lock file so any UI knows we are running
3361        if os.path.exists(os.path.join(logpath, "mythburn.lck")):
3362            write("Lock File Exists - already running???")
3363            sys.exit(1)
3364
3365        file = open(os.path.join(logpath, "mythburn.lck"), 'w')
3366        file.write("lock")
3367        file.close()
3368
3369        #debug use
3370        #videomode="ntsc"
3371
3372        getTimeDateFormats()
3373
3374        #Load XML input file from disk
3375        jobDOM = xml.dom.minidom.parse(jobfile)
3376
3377        #Error out if its the wrong XML
3378        if jobDOM.documentElement.tagName != "mythburn":
3379            fatalError("Job file doesn't look right!")
3380
3381        #process each job
3382        jobcount=0
3383        jobs=jobDOM.getElementsByTagName("job")
3384        for job in jobs:
3385            jobcount+=1
3386            write( "Processing Mythburn job number %s." % jobcount)
3387
3388            #get any options from the job file if present
3389            options = job.getElementsByTagName("options")
3390            if options.length > 0:
3391                getOptions(options)
3392
3393            processJob(job)
3394
3395        jobDOM.unlink()
3396
3397        write("Finished processing jobs!!!")
3398    finally:
3399        # remove our lock file
3400        if os.path.exists(os.path.join(logpath, "mythburn.lck")):
3401            os.remove(os.path.join(logpath, "mythburn.lck"))
3402
3403        # make sure the files we created are read/writable by all
3404        os.system("chmod -R a+rw-x+X %s" % defaultsettings["MythArchiveTempDir"])
3405except SystemExit:
3406    write("Terminated")
3407except:
3408    write('-'*60)
3409    traceback.print_exc(file=sys.stdout)
3410    if progresslog != "":
3411        traceback.print_exc(file=progressfile)
3412    write('-'*60)