MythTV  0.26-pre
mythburn.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 # -*- coding: utf-8 -*-
00003 # mythburn.py
00004 # The ported MythBurn scripts which feature:
00005 
00006 # Burning of recordings (including HDTV) and videos
00007 # of ANY format to DVDR.  Menus are created using themes
00008 # and are easily customised.
00009 
00010 # See mydata.xml for format of input file
00011 
00012 # spit2k1
00013 # 11 January 2006
00014 # 6 Feb 2006 - Added into CVS for the first time
00015 
00016 # paulh
00017 # 4 May 2006 - Added into mythtv svn
00018 
00019 # For this script to work you need to have...
00020 # Python - v2.6 or later
00021 # mythtv python bindings installed
00022 # python-imaging (PIL)
00023 # dvdauthor - v0.6.14
00024 # dvd+rw-tools - v7.1
00025 # cdrtools - v3.01
00026 
00027 # Optional for shrink-to-fit requantisation
00028 # M2VRequantiser (from flexion, based on the newer code from Metakine)
00029 
00030 # Optional (for Right To Left languages)
00031 # pyfribidi
00032 
00033 # Optional (alternate demuxer)
00034 # ProjectX - >=0.91
00035 
00036 #******************************************************************************
00037 #******************************************************************************
00038 #******************************************************************************
00039 
00040 
00041 # All strings in this file should be unicode, not byte string!! They get converted to utf-8 only
00042 
00043 
00044 
00045 
00046 # version of script - change after each update
00047 VERSION="0.1.20120304-1"
00048 
00049 # keep all temporary files for debugging purposes
00050 # set this to True before a first run through when testing
00051 # out new themes (see below)
00052 debug_keeptempfiles = False
00053 
00054 ##You can use this debug flag when testing out new themes
00055 ##pick some small recordings, run them through as normal
00056 ##with debug_keeptempfiles = True (see above)
00057 ##set this variable to True and then re-run the scripts
00058 ##the temp. files will not be deleted and it will run through
00059 ##very much quicker!
00060 debug_secondrunthrough = False
00061 
00062 # default encoding profile to use
00063 defaultEncodingProfile = "SP"
00064 
00065 # add audio sync offset when re-muxing
00066 useSyncOffset = True
00067 
00068 # if the theme doesn't have a chapter menu and this is set to true then the
00069 # chapter marks will be set to the cut point end marks
00070 addCutlistChapters = False
00071 
00072 # by default we always convert any audio tracks to ac3 for better compatibility
00073 encodetoac3 = True
00074 
00075 #*********************************************************************************
00076 #Dont change the stuff below!!
00077 #*********************************************************************************
00078 import os
00079 import sys
00080 import string
00081 import getopt
00082 import traceback
00083 import signal
00084 import xml.dom.minidom
00085 import Image
00086 import ImageDraw
00087 import ImageFont
00088 import ImageColor
00089 import unicodedata
00090 import time
00091 import datetime
00092 import tempfile
00093 from fcntl import ioctl
00094 import CDROM
00095 from shutil import copy
00096 
00097 import MythTV
00098 from MythTV.altdict import OrdDict
00099 
00100 # media types (should match the enum in mytharchivewizard.h)
00101 DVD_SL = 0
00102 DVD_DL = 1
00103 DVD_RW = 2
00104 FILE   = 3
00105 
00106 dvdPAL=(720,576)
00107 dvdNTSC=(720,480)
00108 dvdPALdpi=(75,80)
00109 dvdNTSCdpi=(81,72)
00110 
00111 dvdPALHalfD1="352x576"
00112 dvdNTSCHalfD1="352x480"
00113 dvdPALD1="%sx%s" % (dvdPAL[0],dvdPAL[1])
00114 dvdNTSCD1="%sx%s" % (dvdNTSC[0],dvdNTSC[1])
00115 
00116 #Single and dual layer recordable DVD free space in MBytes
00117 dvdrsize=(4482,8106)
00118 
00119 frameratePAL=25
00120 framerateNTSC=29.97
00121 
00122 #any aspect ratio above this value is assumed to be 16:9
00123 aspectRatioThreshold = 1.4
00124 
00125 #Just blank globals at startup
00126 temppath=""
00127 logpath=""
00128 scriptpath=""
00129 sharepath=""
00130 videopath=""
00131 defaultsettings=""
00132 videomode=""
00133 gallerypath=""
00134 musicpath=""
00135 dateformat=""
00136 timeformat=""
00137 dbVersion=""
00138 preferredlang1=""
00139 preferredlang2=""
00140 useFIFO = True
00141 alwaysRunMythtranscode = False
00142 copyremoteFiles = False
00143 thumboffset = 10
00144 usebookmark = True
00145 clearArchiveTable = True
00146 nicelevel = 17;
00147 drivespeed = 0;
00148 
00149 #main menu aspect ratio (4:3 or 16:9)
00150 mainmenuAspectRatio = "16:9"
00151 
00152 #chapter menu aspect ratio (4:3, 16:9 or Video) 
00153 #video means same aspect ratio as the video title
00154 chaptermenuAspectRatio = "Video"
00155 
00156 #default chapter length in seconds
00157 chapterLength = 5 * 60;
00158 
00159 #name of the default job file
00160 jobfile="mydata.xml"
00161 
00162 #progress log filename and file object
00163 progresslog = ""
00164 progressfile = open("/dev/null", 'w')
00165 
00166 #default location of DVD drive
00167 dvddrivepath = "/dev/dvd"
00168 
00169 #default option settings
00170 docreateiso = False
00171 doburn = True
00172 erasedvdrw = False
00173 mediatype = DVD_SL
00174 savefilename = ''
00175 
00176 installPrefix = ""
00177 
00178 # job xml file
00179 jobDOM = None
00180 
00181 # theme xml file
00182 themeDOM = None
00183 themeName = ''
00184 
00185 #dictionary of font definitions used in theme
00186 themeFonts = {}
00187 
00188 # no. of processors we have access to
00189 cpuCount = 1
00190 
00191 DB = MythTV.MythDB()
00192 MVID = MythTV.MythVideo(db=DB)
00193 
00194 configHostname = DB.gethostname()
00195 
00196 #############################################################
00197 
00198 # fix rtl text where pyfribidi is not available
00199 # should write a simple algorithm, meanwhile just return the original string
00200 def simple_fix_rtl(str):
00201   return str
00202 
00203 # Bind the name fix_rtl to the appropriate function
00204 try:
00205     import pyfribidi
00206 except ImportError:
00207     sys.stdout.write("Using simple_fix_rtl\n")
00208     fix_rtl = simple_fix_rtl
00209 else:
00210     sys.stdout.write("Using pyfribidi.log2vis\n")
00211     fix_rtl = pyfribidi.log2vis
00212 
00213 #############################################################
00214 # class to hold a font definition
00215 
00216 class FontDef(object):
00217     def __init__(self, name=None, fontFile=None, size=19, color="white", effect="normal", shadowColor="black", shadowSize=1):
00218         self.name = name
00219         self.fontFile = fontFile
00220         self.size = size
00221         self.color = color
00222         self.effect = effect
00223         self.shadowColor = shadowColor
00224         self.shadowSize = shadowSize
00225         self.font = None
00226 
00227     def getFont(self):
00228         if self.font == None:
00229             self.font = ImageFont.truetype(self.fontFile, int(self.size))
00230 
00231         return self.font
00232 
00233     def drawText(self, text, color=None):
00234         if self.font == None:
00235             self.font = ImageFont.truetype(self.fontFile, int(self.size))
00236 
00237         if color == None:
00238             color = self.color
00239 
00240         textwidth, textheight = self.font.getsize(text)
00241 
00242         image = Image.new("RGBA", (textwidth + (self.shadowSize * 2), textheight), (0,0,0,0))
00243         draw = ImageDraw.ImageDraw(image)
00244 
00245         if self.effect == "shadow":
00246             draw.text((self.shadowSize,self.shadowSize), text, font=self.font, fill=self.shadowColor)
00247             draw.text((0,0), text, font=self.font, fill=color)
00248         elif self.effect == "outline":
00249             for x in range(0, self.shadowSize * 2 + 1):
00250                 for y in range(0, self.shadowSize * 2 + 1):
00251                     draw.text((x, y), text, font=self.font, fill=self.shadowColor)
00252 
00253             draw.text((self.shadowSize,self.shadowSize), text, font=self.font, fill=color)
00254         else:
00255             draw.text((0,0), text, font=self.font, fill=color)
00256 
00257         bbox = image.getbbox()
00258         image = image.crop(bbox)
00259         return image
00260 
00261 #############################################################
00262 # Write a string to stdout and optionaly to a progress log file
00263 
00264 def write(text, progress=True):
00265     """Simple place to channel all text output through"""
00266 
00267     text = text.encode("utf-8", "replace")
00268     sys.stdout.write(text + "\n")
00269     sys.stdout.flush()
00270 
00271     if progress == True and progresslog != "":
00272         progressfile.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n")
00273         progressfile.flush()
00274 
00275 #############################################################
00276 # Display an error message and exit
00277 
00278 def fatalError(msg):
00279     """Display an error message and exit app"""
00280     write("*"*60)
00281     write("ERROR: " + msg)
00282     write("See mythburn.log for more information.")
00283     write("*"*60)
00284     write("")
00285     saveSetting("MythArchiveLastRunResult", "Failed: " + quoteString(msg));
00286     saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
00287     sys.exit(0)
00288 
00289 # ###########################################################
00290 # Display a warning message
00291 
00292 def nonfatalError(msg):
00293     """Display a warning message"""
00294     write("*"*60)
00295     write("WARNING: " + msg)
00296     write("*"*60)
00297     write("")
00298 
00299 #############################################################
00300 # Return the input string with single quotes escaped.
00301 
00302 def quoteString(str):
00303      """Return the input string with single quotes escaped."""
00304      return str.replace("'", "'\"'\"'")
00305 
00306 #############################################################
00307 # Directory where all temporary files will be created.
00308 
00309 def getTempPath():
00310     """This is the folder where all temporary files will be created."""
00311     return temppath
00312 
00313 #############################################################
00314 # Try to work out how many cpus we have available
00315 
00316 def getCPUCount():
00317     """return the number of CPUs"""
00318     cpustat = open("/proc/cpuinfo")
00319     cpudata = cpustat.readlines()
00320     cpustat.close()
00321 
00322     cpucount = 0
00323     for line in cpudata:
00324         tokens = line.split()
00325         if len(tokens) > 0:
00326             if tokens[0] == "processor":
00327                 cpucount += 1
00328 
00329     if cpucount == 0:
00330         cpucount = 1
00331 
00332     write("Found %d CPUs" % cpucount)
00333 
00334     return cpucount
00335 
00336 #############################################################
00337 # Get the directory where all encoder profile files are located.
00338 
00339 def getEncodingProfilePath():
00340     """This is the folder where all encoder profile files are located."""
00341     return os.path.join(sharepath, "mytharchive", "encoder_profiles")
00342 
00343 #############################################################
00344 # Returns true/false if a given file or path exists.
00345 
00346 def doesFileExist(file):
00347     """Returns true/false if a given file or path exists."""
00348     return os.path.exists( file )
00349 
00350 #############################################################
00351 # Escape quotes in a command line argument
00352 
00353 def quoteCmdArg(arg):
00354     arg = arg.replace('"', '\\"')
00355     arg = arg.replace('`', '\\`')
00356     return '"%s"' % arg
00357 
00358 #############################################################
00359 # Returns the text contents from a given XML element.
00360 
00361 def getText(node):
00362     """Returns the text contents from a given XML element."""
00363     if node.childNodes.length>0:
00364         return node.childNodes[0].data
00365     else:
00366         return ""
00367 
00368 #############################################################
00369 # Try to find a theme file
00370 
00371 def getThemeFile(theme,file):
00372     """Find a theme file - first look in the specified theme directory then look in the
00373        shared music and image directories"""
00374     if os.path.exists(os.path.join(sharepath, "mytharchive", "themes", theme, file)):
00375         return os.path.join(sharepath, "mytharchive", "themes", theme, file)
00376 
00377     if os.path.exists(os.path.join(sharepath, "mytharchive", "images", file)):
00378         return os.path.join(sharepath, "mytharchive", "images", file)
00379 
00380     if os.path.exists(os.path.join(sharepath, "mytharchive", "intro", file)):
00381         return os.path.join(sharepath, "mytharchive", "intro", file)
00382 
00383     if os.path.exists(os.path.join(sharepath, "mytharchive", "music", file)):
00384         return os.path.join(sharepath, "mytharchive", "music", file)
00385 
00386     fatalError("Cannot find theme file '%s' in theme '%s'" % (file, theme))
00387 
00388 #############################################################
00389 # Returns the path where we can find our fonts
00390 
00391 def getFontPathName(fontname):
00392     return os.path.join(sharepath, "fonts", fontname)
00393 
00394 #############################################################
00395 # Creates a file path where the temp files for a video file can be created
00396 
00397 def getItemTempPath(itemnumber):
00398     return os.path.join(getTempPath(),"%s" % itemnumber)
00399 
00400 #############################################################
00401 # Returns True if the theme.xml file can be found for the given theme
00402 
00403 def validateTheme(theme):
00404     #write( "Checking theme", theme
00405     file = getThemeFile(theme,"theme.xml")
00406     write("Looking for: " + file)
00407     return doesFileExist( getThemeFile(theme,"theme.xml") )
00408 
00409 #############################################################
00410 # Returns True if the given resolution is a DVD compliant one
00411 
00412 def isResolutionOkayForDVD(videoresolution):
00413     if videomode=="ntsc":
00414         return videoresolution==(720,480) or videoresolution==(704,480) or videoresolution==(352,480) or videoresolution==(352,240)
00415     else:
00416         return videoresolution==(720,576) or videoresolution==(704,576) or videoresolution==(352,576) or videoresolution==(352,288)
00417 
00418 #############################################################
00419 # Removes all the files from a directory
00420 
00421 def deleteAllFilesInFolder(folder):
00422     """Does what it says on the tin!."""
00423     for root, dirs, deletefiles in os.walk(folder, topdown=False):
00424         for name in deletefiles:
00425                 os.remove(os.path.join(root, name))
00426 
00427 #############################################################
00428 # Romoves all the objects from a directory
00429 
00430 def deleteEverythingInFolder(folder):
00431     for root, dirs, files in os.walk(folder, topdown=False):
00432         for name in files:
00433                 os.remove(os.path.join(root, name))
00434         for name in dirs:
00435                 if os.path.islink(os.path.join(root, name)):
00436                     os.remove(os.path.join(root, name))
00437                 else:
00438                     os.rmdir(os.path.join(root, name))
00439 
00440 #############################################################
00441 # Check to see if the user has cancelled the DVD creation process
00442 
00443 def checkCancelFlag():
00444     """Checks to see if the user has cancelled this run"""
00445     if os.path.exists(os.path.join(logpath, "mythburncancel.lck")):
00446         os.remove(os.path.join(logpath, "mythburncancel.lck"))
00447         write('*'*60)
00448         write("Job has been cancelled at users request")
00449         write('*'*60)
00450         sys.exit(1)
00451 
00452 #############################################################
00453 # Runs an external command checking to see if the user has cancelled
00454 # the DVD creation process
00455 
00456 def runCommand(command):
00457     checkCancelFlag()
00458 
00459     # mytharchivehelper needes this locale to work correctly
00460     try:
00461        oldlocale = os.environ["LC_ALL"]
00462     except:
00463        oldlocale = ""
00464     os.putenv("LC_ALL", "en_US.UTF-8")
00465     result = os.system(command.encode('utf-8'))
00466     os.putenv("LC_ALL", oldlocale)
00467 
00468     if os.WIFEXITED(result):
00469         result = os.WEXITSTATUS(result)
00470     checkCancelFlag()
00471     return result
00472 
00473 #############################################################
00474 # Convert a time in seconds to a frame number
00475 
00476 def secondsToFrames(seconds):
00477     """Convert a time in seconds to a frame position"""
00478     if videomode=="pal":
00479         framespersecond=frameratePAL
00480     else:
00481         framespersecond=framerateNTSC
00482 
00483     frames=int(seconds * framespersecond)
00484     return frames
00485 
00486 #############################################################
00487 # Creates a short mpeg file from a jpeg image and an ac3 sound track
00488 
00489 def encodeMenu(background, tempvideo, music, musiclength, tempmovie, xmlfile, finaloutput, aspectratio):
00490     if videomode=="pal":
00491         framespersecond=frameratePAL
00492     else:
00493         framespersecond=framerateNTSC
00494 
00495     totalframes=int(musiclength * framespersecond)
00496 
00497     command = quoteCmdArg(path_jpeg2yuv[0]) + " -n %s -v0 -I p -f %s -j %s | %s -b 5000 -a %s -v 1 -f 8 -o %s" \
00498               % (totalframes, framespersecond, quoteCmdArg(background), quoteCmdArg(path_mpeg2enc[0]), aspectratio, quoteCmdArg(tempvideo))
00499     result = runCommand(command)
00500     if result<>0:
00501         fatalError("Failed while running jpeg2yuv - %s" % command)
00502 
00503     command = quoteCmdArg(path_mplex[0]) + " -f 8 -v 0 -o %s %s %s" % (quoteCmdArg(tempmovie), quoteCmdArg(tempvideo), quoteCmdArg(music))
00504     result = runCommand(command)
00505     if result<>0:
00506         fatalError("Failed while running mplex - %s" % command)
00507 
00508     if xmlfile != "":
00509         command = quoteCmdArg(path_spumux[0]) + " -m dvd -s 0 %s < %s > %s" % (quoteCmdArg(xmlfile), quoteCmdArg(tempmovie), quoteCmdArg(finaloutput))
00510         result = runCommand(command)
00511         if result<>0:
00512             fatalError("Failed while running spumux - %s" % command)
00513     else:
00514         os.rename(tempmovie, finaloutput)
00515 
00516     if os.path.exists(tempvideo):
00517             os.remove(tempvideo)
00518     if os.path.exists(tempmovie):
00519             os.remove(tempmovie)
00520 
00521 #############################################################
00522 # Return an xml node from a re-encoding profile xml file for 
00523 # a given profile name
00524 
00525 def findEncodingProfile(profile):
00526     """Returns the XML node for the given encoding profile"""
00527 
00528     # which encoding file do we need
00529 
00530     # first look for a custom profile file in ~/.mythtv/MythArchive/
00531     if videomode == "ntsc":
00532         filename = os.path.expanduser("~/.mythtv/MythArchive/ffmpeg_dvd_ntsc.xml")
00533     else:
00534         filename = os.path.expanduser("~/.mythtv/MythArchive/ffmpeg_dvd_pal.xml")
00535 
00536     if not os.path.exists(filename):
00537         # not found so use the default profiles
00538         if videomode == "ntsc":
00539             filename = getEncodingProfilePath() + "/ffmpeg_dvd_ntsc.xml"
00540         else:
00541             filename = getEncodingProfilePath() + "/ffmpeg_dvd_pal.xml"
00542 
00543     write("Using encoder profiles from %s" % filename)
00544 
00545     DOM = xml.dom.minidom.parse(filename)
00546 
00547     #Error out if its the wrong XML
00548     if DOM.documentElement.tagName != "encoderprofiles":
00549         fatalError("Profile xml file doesn't look right (%s)" % filename)
00550 
00551     profiles = DOM.getElementsByTagName("profile")
00552     for node in profiles:
00553         if getText(node.getElementsByTagName("name")[0]) == profile:
00554             write("Encoding profile (%s) found" % profile)
00555             return node
00556 
00557     fatalError("Encoding profile (%s) not found" % profile)
00558     return None
00559 
00560 #############################################################
00561 # Load the theme.xml file for a DVD theme
00562 
00563 def getThemeConfigurationXML(theme):
00564     """Loads the XML file from disk for a specific theme"""
00565 
00566     #Load XML input file from disk
00567     themeDOM = xml.dom.minidom.parse( getThemeFile(theme,"theme.xml") )
00568     #Error out if its the wrong XML
00569     if themeDOM.documentElement.tagName != "mythburntheme":
00570         fatalError("Theme xml file doesn't look right (%s)" % theme)
00571     return themeDOM
00572 
00573 #############################################################
00574 # Gets the duration of a video file from its stream info file
00575 
00576 def getLengthOfVideo(index):
00577     """Returns the length of a video file (in seconds)"""
00578 
00579     #open the XML containing information about this file
00580     infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00581 
00582     #error out if its the wrong XML
00583     if infoDOM.documentElement.tagName != "file":
00584         fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00585     file = infoDOM.getElementsByTagName("file")[0]
00586     if file.attributes["cutduration"].value != 'N/A':
00587         duration = int(file.attributes["cutduration"].value)
00588     else:
00589         duration = 0;
00590 
00591     return duration
00592 
00593 #############################################################
00594 # Gets the audio sample rate and number of channels of a video file 
00595 # from its stream info file
00596 
00597 def getAudioParams(folder):
00598     """Returns the audio bitrate and no of channels for a file from its streaminfo.xml"""
00599 
00600     #open the XML containing information about this file
00601     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
00602 
00603     #error out if its the wrong XML
00604     if infoDOM.documentElement.tagName != "file":
00605         fatalError("Stream info file doesn't look right (%s)" % os.path.join(folder, 'streaminfo.xml'))
00606     audio = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("audio")[0]
00607 
00608     samplerate = audio.attributes["samplerate"].value
00609     channels = audio.attributes["channels"].value
00610 
00611     return (samplerate, channels)
00612 
00613 #############################################################
00614 # Gets the video resolution, frames per second and aspect ratio
00615 # of a video file from its stream info file
00616 
00617 def getVideoParams(folder):
00618     """Returns the video resolution, fps and aspect ratio for the video file from the streamindo.xml file"""
00619 
00620     #open the XML containing information about this file
00621     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
00622 
00623     #error out if its the wrong XML
00624     if infoDOM.documentElement.tagName != "file":
00625         fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00626     video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
00627 
00628     if video.attributes["aspectratio"].value != 'N/A':
00629         aspect_ratio = video.attributes["aspectratio"].value
00630     else:
00631         aspect_ratio = "1.77778" 
00632 
00633     videores = video.attributes["width"].value + 'x' + video.attributes["height"].value
00634     fps = video.attributes["fps"].value
00635 
00636     #sanity check the fps
00637     if videomode=="pal":
00638         fr=frameratePAL
00639     else:
00640         fr=framerateNTSC
00641 
00642     if float(fr) != float(fps):
00643         write("WARNING: frames rates do not match")
00644         write("The frame rate for %s should be %s but the stream info file "
00645               "report a fps of %s" % (videomode, fr, fps))
00646         fps = fr
00647 
00648     return (videores, fps, aspect_ratio)
00649 
00650 #############################################################
00651 # Gets the aspect ratio of a video file from its stream info file
00652 
00653 def getAspectRatioOfVideo(index):
00654     """Returns the aspect ratio of the video file (1.333, 1.778, etc)"""
00655 
00656     #open the XML containing information about this file
00657     infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00658 
00659     #error out if its the wrong XML
00660     if infoDOM.documentElement.tagName != "file":
00661         fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00662     video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
00663     if video.attributes["aspectratio"].value != 'N/A':
00664         aspect_ratio = float(video.attributes["aspectratio"].value)
00665     else:
00666         aspect_ratio = 1.77778; # default
00667     write("aspect ratio is: %s" % aspect_ratio)
00668     return aspect_ratio
00669 
00670 #############################################################
00671 # Calculates the sync offset between the video and first audio stream
00672 
00673 def calcSyncOffset(index):
00674     """Returns the sync offset between the video and first audio stream"""
00675 
00676     #open the XML containing information about this file
00677     #infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo_orig.xml'))
00678     infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
00679 
00680     #error out if its the wrong XML
00681     if infoDOM.documentElement.tagName != "file":
00682         fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo_orig.xml'))
00683 
00684     video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
00685     video_start = float(video.attributes["start_time"].value)
00686 
00687     audio = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("audio")[0]
00688     audio_start = float(audio.attributes["start_time"].value)
00689 
00690 #    write("Video start time is: %s" % video_start)
00691 #    write("Audio start time is: %s" % audio_start)
00692 
00693     sync_offset = int((video_start - audio_start) * 1000)
00694 
00695 #    write("Sync offset is: %s" % sync_offset)
00696     return sync_offset
00697 
00698 #############################################################
00699 # Gets the length of a video file and returns it as a string
00700 
00701 def getFormatedLengthOfVideo(index):
00702     duration = getLengthOfVideo(index)
00703 
00704     minutes = int(duration / 60)
00705     seconds = duration % 60
00706     hours = int(minutes / 60)
00707     minutes %= 60
00708 
00709     return '%02d:%02d:%02d' % (hours, minutes, seconds)
00710 
00711 #############################################################
00712 # Convert a frame number to a time string
00713 
00714 def frameToTime(frame, fps):
00715     sec = int(frame / fps)
00716     frame = frame - int(sec * fps)
00717     mins = sec / 60
00718     sec %= 60
00719     hour = mins / 60
00720     mins %= 60
00721 
00722     return '%02d:%02d:%02d' % (hour, mins, sec)
00723 
00724 #############################################################
00725 # Creates a set of chapter points evenly spread thoughout a file
00726 # Optionally grabs the thumbnails from the file
00727 
00728 def createVideoChapters(itemnum, numofchapters, lengthofvideo, getthumbnails):
00729     """Returns numofchapters chapter marks even spaced through a certain time period"""
00730 
00731     # if there are user defined thumb images already available use them
00732     infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum),"info.xml"))
00733     thumblistNode = infoDOM.getElementsByTagName("thumblist")
00734     if thumblistNode.length > 0:
00735         thumblist = getText(thumblistNode[0])
00736         write("Using user defined thumb images - %s" % thumblist)
00737         return thumblist
00738 
00739     # no user defined thumbs so create them
00740     segment=int(lengthofvideo / numofchapters)
00741 
00742     write( "Video length is %s seconds. Each chapter will be %s seconds" % (lengthofvideo,segment))
00743 
00744     chapters=[]
00745 
00746     thumbList=[]
00747     starttime=0
00748     count=1
00749     while count<=numofchapters:
00750         chapters.append(time.strftime("%H:%M:%S",time.gmtime(starttime)))
00751 
00752         if starttime==0:
00753             if thumboffset < segment:
00754                 thumbList.append(str(thumboffset))
00755             else:
00756                 thumbList.append(str(starttime))
00757         else:
00758             thumbList.append(str(starttime))
00759 
00760         starttime+=segment
00761         count+=1
00762 
00763     chapters = ','.join(chapters)
00764     thumbList = ','.join(thumbList)
00765 
00766     if getthumbnails==True:
00767         extractVideoFrames( os.path.join(getItemTempPath(itemnum),"stream.mv2"),
00768             os.path.join(getItemTempPath(itemnum),"chapter-%1.jpg"), thumbList)
00769 
00770     return chapters
00771 
00772 #############################################################
00773 # Creates some fixed length chapter marks
00774 
00775 def createVideoChaptersFixedLength(itemnum, segment, lengthofvideo): 
00776     """Returns chapter marks at cut list ends, 
00777        or evenly spaced chapters 'segment' seconds through the file"""
00778 
00779 
00780     if addCutlistChapters == True:
00781         # we've been asked to use the cut list as chapter marks
00782         # so if there is a cut list available, use it
00783 
00784         infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum),"info.xml"))
00785         chapterlistNode = infoDOM.getElementsByTagName("chapterlist")
00786         if chapterlistNode.length > 0:
00787             chapterlist = getText(chapterlistNode[0])
00788             write("Using commercial end marks - %s" % chapterlist)
00789             return chapterlist
00790 
00791     if lengthofvideo < segment:
00792         return "00:00:00"
00793 
00794     numofchapters = lengthofvideo / segment + 1;
00795     chapters = "00:00:00"
00796     starttime = 0
00797     count = 2
00798     while count <= numofchapters:
00799         starttime += segment
00800         chapters += "," + time.strftime("%H:%M:%S", time.gmtime(starttime))
00801         count += 1
00802 
00803     write("Fixed length chapters: %s" % chapters)
00804 
00805     return chapters
00806 
00807 #############################################################
00808 # Reads a load of settings from DB
00809 
00810 def getDefaultParametersFromMythTVDB():
00811     """Reads settings from MythTV database"""
00812 
00813     write( "Obtaining MythTV settings from MySQL database for hostname " + configHostname)
00814 
00815     #DBSchemaVer, ISO639Language0 ISO639Language1 are not dependant upon the hostname.
00816     sqlstatement="""SELECT value, data FROM settings WHERE value IN(
00817                         'DBSchemaVer',
00818                         'ISO639Language0',
00819                         'ISO639Language1') 
00820                     OR (hostname=%s AND value IN(
00821                         'VideoStartupDir',
00822                         'GalleryDir',
00823                         'MusicLocation',
00824                         'MythArchiveVideoFormat',
00825                         'MythArchiveTempDir',
00826                         'MythArchiveMplexCmd',
00827                         'MythArchiveDvdauthorCmd',
00828                         'MythArchiveMkisofsCmd',
00829                         'MythArchiveM2VRequantiserCmd',
00830                         'MythArchiveMpg123Cmd',
00831                         'MythArchiveProjectXCmd',
00832                         'MythArchiveDVDLocation',
00833                         'MythArchiveGrowisofsCmd',
00834                         'MythArchiveJpeg2yuvCmd',
00835                         'MythArchiveSpumuxCmd',
00836                         'MythArchiveMpeg2encCmd',
00837                         'MythArchiveCopyRemoteFiles',
00838                         'MythArchiveAlwaysUseMythTranscode',
00839                         'MythArchiveUseProjectX',
00840                         'MythArchiveAddSubtitles',
00841                         'MythArchiveUseFIFO',
00842                         'MythArchiveMainMenuAR',
00843                         'MythArchiveChapterMenuAR',
00844                         'MythArchiveDateFormat',
00845                         'MythArchiveTimeFormat',
00846                         'MythArchiveClearArchiveTable',
00847                         'MythArchiveDriveSpeed',
00848                         'JobQueueCPU'
00849                         )) ORDER BY value"""
00850 
00851     # create a cursor
00852     cursor = DB.cursor()
00853     # execute SQL statement
00854     cursor.execute(sqlstatement, configHostname)
00855     # get the resultset as a tuple
00856     result = cursor.fetchall()
00857 
00858     cfg = {}
00859     for i in range(len(result)):
00860        cfg[result[i][0]] = result[i][1]
00861 
00862     #bail out if we can't find the temp dir setting
00863     if not "MythArchiveTempDir" in cfg:
00864         fatalError("Can't find the setting for the temp directory. \nHave you run setup in the frontend?")
00865     return cfg
00866 
00867 #############################################################
00868 # Save a setting to the settings table in the DB
00869 
00870 def saveSetting(name, data):
00871     host = DB.gethostname()
00872     DB.settings[host][name] = data
00873 
00874 #############################################################
00875 # Remove all archive items from the archiveitems DB table
00876 
00877 def clearArchiveItems():
00878     ''' Remove all archive items from the archiveitems DB table'''
00879 
00880     write("Removing all archive items from the archiveitems DB table")
00881     with DB as cursor:
00882         cursor.execute("DELETE FROM archiveitems")
00883 
00884 #############################################################
00885 # Load the options from the options node passed in the job file
00886 
00887 def getOptions(options):
00888     global doburn
00889     global docreateiso
00890     global erasedvdrw
00891     global mediatype
00892     global savefilename
00893 
00894     if options.length == 0:
00895         fatalError("Trying to read the options from the job file but none found?")
00896     options = options[0]
00897 
00898     doburn = options.attributes["doburn"].value != '0'
00899     docreateiso = options.attributes["createiso"].value != '0'
00900     erasedvdrw = options.attributes["erasedvdrw"].value != '0'
00901     mediatype = int(options.attributes["mediatype"].value)
00902     savefilename = options.attributes["savefilename"].value
00903 
00904     write("Options - mediatype = %d, doburn = %d, createiso = %d, erasedvdrw = %d" \
00905            % (mediatype, doburn, docreateiso, erasedvdrw))
00906     write("          savefilename = '%s'" % savefilename)
00907 
00908 #############################################################
00909 # Substitutes some text from a theme file with the required values
00910 
00911 def expandItemText(infoDOM, text, itemnumber, pagenumber, keynumber,chapternumber, chapterlist ):
00912     """Replaces keywords in a string with variables from the XML and filesystem"""
00913     text=string.replace(text,"%page","%s" % pagenumber)
00914 
00915     #See if we can use the thumbnail/cover file for videos if there is one.
00916     if getText( infoDOM.getElementsByTagName("coverfile")[0]) =="":
00917         text=string.replace(text,"%thumbnail", os.path.join( getItemTempPath(itemnumber), "title.jpg"))
00918     else:
00919         text=string.replace(text,"%thumbnail", getText( infoDOM.getElementsByTagName("coverfile")[0]) )
00920 
00921     text=string.replace(text,"%itemnumber","%s" % itemnumber )
00922     text=string.replace(text,"%keynumber","%s" % keynumber )
00923 
00924     text=string.replace(text,"%title",getText( infoDOM.getElementsByTagName("title")[0]) )
00925     text=string.replace(text,"%subtitle",getText( infoDOM.getElementsByTagName("subtitle")[0]) )
00926     text=string.replace(text,"%description",getText( infoDOM.getElementsByTagName("description")[0]) )
00927     text=string.replace(text,"%type",getText( infoDOM.getElementsByTagName("type")[0]) )
00928 
00929     text=string.replace(text,"%recordingdate",getText( infoDOM.getElementsByTagName("recordingdate")[0]) )
00930     text=string.replace(text,"%recordingtime",getText( infoDOM.getElementsByTagName("recordingtime")[0]) )
00931 
00932     text=string.replace(text,"%duration", getFormatedLengthOfVideo(itemnumber))
00933 
00934     text=string.replace(text,"%myfolder",getThemeFile(themeName,""))
00935 
00936     if chapternumber>0:
00937         text=string.replace(text,"%chapternumber","%s" % chapternumber )
00938         text=string.replace(text,"%chaptertime","%s" % chapterlist[chapternumber - 1] )
00939         text=string.replace(text,"%chapterthumbnail", os.path.join( getItemTempPath(itemnumber), "chapter-%s.jpg" % chapternumber))
00940 
00941     return text
00942 
00943 #############################################################
00944 # Scale a theme position/size depending on the current video mode
00945 
00946 def getScaledAttribute(node, attribute):
00947     """ Returns a value taken from attribute in node scaled for the current video mode"""
00948 
00949     if videomode == "pal" or attribute == "x" or attribute == "w":
00950         return int(node.attributes[attribute].value)
00951     else:
00952         return int(float(node.attributes[attribute].value) / 1.2)
00953 
00954 #############################################################
00955 # Splits some text into lines so it will fit into a given container
00956 
00957 def intelliDraw(drawer, text, font, containerWidth):
00958     """Based on http://mail.python.org/pipermail/image-sig/2004-December/003064.html"""
00959     #Args:
00960     #  drawer: Instance of "ImageDraw.Draw()"
00961     #  text: string of long text to be wrapped
00962     #  font: instance of ImageFont (I use .truetype)
00963     #  containerWidth: number of pixels text lines have to fit into.
00964 
00965     #write("containerWidth: %s" % containerWidth)
00966     words = text.split()
00967     lines = [] # prepare a return argument
00968     lines.append(words) 
00969     finished = False
00970     line = 0
00971     while not finished:
00972         thistext = lines[line]
00973         newline = []
00974         innerFinished = False
00975         while not innerFinished:
00976             #write( 'thistext: '+str(thistext))
00977             #write("textWidth: %s" % drawer.textsize(' '.join(thistext),font)[0])
00978 
00979             if drawer.textsize(' '.join(thistext),font.getFont())[0] > containerWidth:
00980                 # this is the heart of the algorithm: we pop words off the current
00981                 # sentence until the width is ok, then in the next outer loop
00982                 # we move on to the next sentence. 
00983                 if str(thistext).find(' ') != -1:
00984                     newline.insert(0,thistext.pop(-1))
00985                 else:
00986                     # FIXME should truncate the string here
00987                     innerFinished = True
00988             else:
00989                 innerFinished = True
00990         if len(newline) > 0:
00991             lines.append(newline)
00992             line = line + 1
00993         else:
00994             finished = True
00995     tmp = []
00996     for i in lines:
00997         tmp.append( fix_rtl( ' '.join(i) ) )
00998     lines = tmp
00999     return lines
01000 
01001 #############################################################
01002 # Paints a background rectangle onto an image
01003 
01004 def paintBackground(image, node):
01005     if node.hasAttribute("bgcolor"):
01006         bgcolor = node.attributes["bgcolor"].value
01007         x = getScaledAttribute(node, "x")
01008         y = getScaledAttribute(node, "y")
01009         w = getScaledAttribute(node, "w")
01010         h = getScaledAttribute(node, "h")
01011         r,g,b = ImageColor.getrgb(bgcolor)
01012 
01013         if node.hasAttribute("bgalpha"):
01014             a = int(node.attributes["bgalpha"].value)
01015         else:
01016             a = 255
01017 
01018         image.paste((r, g, b, a), (x, y, x + w, y + h))
01019 
01020 
01021 #############################################################
01022 # Paints a button onto an image
01023 
01024 def paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
01025                 itemsonthispage, chapternumber, chapterlist):
01026 
01027     imagefilename = getThemeFile(themeName, node.attributes["filename"].value)
01028     if not doesFileExist(imagefilename):
01029         fatalError("Cannot find image for menu button (%s)." % imagefilename)
01030     maskimagefilename = getThemeFile(themeName, node.attributes["mask"].value)
01031     if not doesFileExist(maskimagefilename):
01032         fatalError("Cannot find mask image for menu button (%s)." % maskimagefilename)
01033 
01034     picture = Image.open(imagefilename,"r").resize(
01035               (getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
01036     picture = picture.convert("RGBA")
01037     bgimage.paste(picture, (getScaledAttribute(node, "x"),
01038                   getScaledAttribute(node, "y")), picture)
01039     del picture
01040 
01041     # if we have some text paint that over the image
01042     textnode = node.getElementsByTagName("textnormal")
01043     if textnode.length > 0:
01044         textnode = textnode[0]
01045         text = expandItemText(infoDOM,textnode.attributes["value"].value,
01046                               itemnum, page, itemsonthispage,
01047                               chapternumber,chapterlist)
01048 
01049         if text > "":
01050             paintText(draw, bgimage, text, textnode)
01051 
01052         del text
01053 
01054     write( "Added button image %s" % imagefilename)
01055 
01056     picture = Image.open(maskimagefilename,"r").resize(
01057               (getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
01058     picture = picture.convert("RGBA")
01059     bgimagemask.paste(picture, (getScaledAttribute(node, "x"),
01060                       getScaledAttribute(node, "y")),picture)
01061     #del picture
01062 
01063     # if we have some text paint that over the image
01064     textnode = node.getElementsByTagName("textselected")
01065     if textnode.length > 0:
01066         textnode = textnode[0]
01067         text = expandItemText(infoDOM, textnode.attributes["value"].value,
01068                               itemnum, page, itemsonthispage,
01069                               chapternumber, chapterlist)
01070         textImage = Image.new("RGBA",picture.size)
01071         textDraw = ImageDraw.Draw(textImage)
01072 
01073         if text > "":
01074             paintText(textDraw, textImage, text, textnode, "white",
01075                       getScaledAttribute(node, "x") - getScaledAttribute(textnode, "x"),
01076                       getScaledAttribute(node, "y") - getScaledAttribute(textnode, "y"),
01077                       getScaledAttribute(textnode, "w"),
01078                       getScaledAttribute(textnode, "h"))
01079 
01080         #convert the RGB image to a 1 bit image
01081         (width, height) = textImage.size
01082         for y in range(height):
01083             for x in range(width):
01084                 if textImage.getpixel((x,y)) < (100, 100, 100, 255):
01085                     textImage.putpixel((x,y), (0, 0, 0, 0))
01086                 else:
01087                     textImage.putpixel((x,y), (255, 255, 255, 255))
01088 
01089         if textnode.hasAttribute("colour"):
01090             color = textnode.attributes["colour"].value
01091         elif textnode.hasAttribute("color"):
01092             color = textnode.attributes["color"].value
01093         else:
01094             color = "white"
01095 
01096         bgimagemask.paste(color,
01097                          (getScaledAttribute(textnode, "x"),
01098                           getScaledAttribute(textnode, "y")),
01099                           textImage)
01100 
01101         del text, textImage, textDraw
01102     del picture
01103 
01104 #############################################################
01105 # Paint some theme text on to an image
01106 
01107 def paintText(draw, image, text, node, color = None, 
01108               x = None, y = None, width = None, height = None):
01109     """Takes a piece of text and draws it onto an image inside a bounding box."""
01110     #The text is wider than the width of the bounding box
01111 
01112     if x == None:
01113         x = getScaledAttribute(node, "x") 
01114         y = getScaledAttribute(node, "y")
01115         width = getScaledAttribute(node, "w")
01116         height = getScaledAttribute(node, "h")
01117 
01118     font = themeFonts[node.attributes["font"].value]
01119 
01120     if color == None:
01121         if node.hasAttribute("colour"):
01122             color = node.attributes["colour"].value
01123         elif node.hasAttribute("color"):
01124             color = node.attributes["color"].value
01125         else:
01126             color = None
01127 
01128     if node.hasAttribute("halign"):
01129         halign = node.attributes["halign"].value
01130     elif node.hasAttribute("align"):
01131         halign = node.attributes["align"].value
01132     else:
01133         halign = "left"
01134 
01135     if node.hasAttribute("valign"):
01136         valign = node.attributes["valign"].value
01137     else:
01138         valign = "top"
01139 
01140     if node.hasAttribute("vindent"):
01141         vindent = int(node.attributes["vindent"].value)
01142     else:
01143         vindent = 0
01144 
01145     if node.hasAttribute("hindent"):
01146         hindent = int(node.attributes["hindent"].value)
01147     else:
01148         hindent = 0
01149 
01150     lines = intelliDraw(draw, text, font, width - (hindent * 2))
01151     j = 0
01152 
01153     # work out what the line spacing should be
01154     textImage = font.drawText(lines[0])
01155     h = int(textImage.size[1] * 1.1)
01156 
01157     for i in lines:
01158         if (j * h) < (height - (vindent * 2) - h):
01159             textImage = font.drawText(i, color)
01160             write( "Wrapped text  = " + i.encode("ascii", "replace"), False)
01161 
01162             if halign == "left":
01163                 xoffset = hindent
01164             elif  halign == "center" or halign == "centre":
01165                 xoffset = (width / 2) - (textImage.size[0] / 2)
01166             elif  halign == "right":
01167                 xoffset = width - textImage.size[0] - hindent
01168             else:
01169                 xoffset = hindent
01170 
01171             if valign == "top":
01172                 yoffset = vindent
01173             elif  valign == "center" or halign == "centre":
01174                 yoffset = (height / 2) - (textImage.size[1] / 2)
01175             elif  valign == "bottom":
01176                 yoffset = height - textImage.size[1] - vindent
01177             else:
01178                 yoffset = vindent
01179 
01180             image.paste(textImage, (x + xoffset,y + yoffset + j * h), textImage)
01181         else:
01182             write( "Truncated text = " + i.encode("ascii", "replace"), False)
01183         #Move to next line
01184         j = j + 1
01185 
01186 #############################################################
01187 # Paint an image on the background image
01188 
01189 def paintImage(filename, maskfilename, imageDom, destimage, stretch=True):
01190     """Paste the image specified in the filename into the specified image"""
01191 
01192     if not doesFileExist(filename):
01193         write("Image file (%s) does not exist" % filename)
01194         return False
01195 
01196     picture = Image.open(filename, "r")
01197     xpos = getScaledAttribute(imageDom, "x")
01198     ypos = getScaledAttribute(imageDom, "y")
01199     w = getScaledAttribute(imageDom, "w")
01200     h = getScaledAttribute(imageDom, "h")
01201     (imgw, imgh) = picture.size
01202     write("Image (%s, %s) into space of (%s, %s) at (%s, %s)" % (imgw, imgh, w, h, xpos, ypos), False)
01203 
01204     # the theme can override the default stretch behaviour 
01205     if imageDom.hasAttribute("stretch"): 
01206         if imageDom.attributes["stretch"].value == "True":
01207             stretch = True
01208         else:
01209             stretch = False
01210 
01211     if stretch == True:
01212         imgw = w;
01213         imgh = h;
01214     else:
01215         if float(w)/imgw < float(h)/imgh:
01216             # Width is the constraining dimension
01217             imgh = imgh*w/imgw
01218             imgw = w
01219             if imageDom.hasAttribute("valign"):
01220                 valign = imageDom.attributes["valign"].value
01221             else:
01222                 valign = "center"
01223 
01224             if valign == "bottom":
01225                 ypos += h - imgh
01226             if valign == "center":
01227                 ypos += (h - imgh)/2
01228         else:
01229             # Height is the constraining dimension
01230             imgw = imgw*h/imgh
01231             imgh = h
01232             if imageDom.hasAttribute("halign"):
01233                 halign = imageDom.attributes["halign"].value
01234             else:
01235                 halign = "center"
01236 
01237             if halign == "right":
01238                 xpos += w - imgw
01239             if halign == "center":
01240                 xpos += (w - imgw)/2
01241 
01242     write("Image resized to (%s, %s) at (%s, %s)" % (imgw, imgh, xpos, ypos), False)
01243     picture = picture.resize((imgw, imgh))
01244     picture = picture.convert("RGBA")
01245 
01246     if maskfilename <> None and doesFileExist(maskfilename):
01247         maskpicture = Image.open(maskfilename, "r").resize((imgw, imgh))
01248         maskpicture = maskpicture.convert("RGBA")
01249     else:
01250         maskpicture = picture
01251 
01252     destimage.paste(picture, (xpos, ypos), maskpicture)
01253     del picture
01254     if maskfilename <> None and doesFileExist(maskfilename):
01255         del maskpicture
01256 
01257     write ("Added image %s" % filename)
01258 
01259     return True
01260 
01261 
01262 #############################################################
01263 # Check if boundary box need adjusting
01264 
01265 def checkBoundaryBox(boundarybox, node):
01266     # We work out how much space all of our graphics and text are taking up
01267     # in a bounding rectangle so that we can use this as an automatic highlight
01268     # on the DVD menu   
01269     if getText(node.attributes["static"]) == "False":
01270         if getScaledAttribute(node, "x") < boundarybox[0]:
01271             boundarybox = getScaledAttribute(node, "x"), boundarybox[1], boundarybox[2], boundarybox[3]
01272 
01273         if getScaledAttribute(node, "y") < boundarybox[1]:
01274             boundarybox = boundarybox[0], getScaledAttribute(node, "y"), boundarybox[2], boundarybox[3]
01275 
01276         if (getScaledAttribute(node, "x") + getScaledAttribute(node, "w")) > boundarybox[2]:
01277             boundarybox = boundarybox[0], boundarybox[1], getScaledAttribute(node, "x") + \
01278                           getScaledAttribute(node, "w"), boundarybox[3]
01279 
01280         if (getScaledAttribute(node, "y") + getScaledAttribute(node, "h")) > boundarybox[3]:
01281             boundarybox = boundarybox[0], boundarybox[1], boundarybox[2], \
01282                           getScaledAttribute(node, "y") + getScaledAttribute(node, "h")
01283 
01284     return boundarybox
01285 
01286 #############################################################
01287 # Load the font defintions from a DVD theme file
01288 
01289 def loadFonts(themeDOM):
01290     global themeFonts
01291 
01292     #Find all the fonts
01293     nodelistfonts = themeDOM.getElementsByTagName("font")
01294 
01295     fontnumber = 0
01296     for node in nodelistfonts:
01297         filename = getText(node)
01298 
01299         if node.hasAttribute("name"):
01300             name = node.attributes["name"].value
01301         else:
01302             name = str(fontnumber)
01303 
01304         fontsize = getScaledAttribute(node, "size")
01305 
01306         if node.hasAttribute("color"):
01307             color = node.attributes["color"].value
01308         else:
01309             color = "white"
01310 
01311         if node.hasAttribute("effect"):
01312             effect = node.attributes["effect"].value
01313         else:
01314             effect = "normal"
01315 
01316         if node.hasAttribute("shadowsize"):
01317             shadowsize = int(node.attributes["shadowsize"].value)
01318         else:
01319             shadowsize = 0
01320 
01321         if node.hasAttribute("shadowcolor"):
01322             shadowcolor = node.attributes["shadowcolor"].value
01323         else:
01324             shadowcolor = "black"
01325 
01326         themeFonts[name] = FontDef(name, getFontPathName(filename),
01327                            fontsize, color, effect, shadowcolor, shadowsize)
01328 
01329         write( "Loading font %s, %s size %s" % (fontnumber,getFontPathName(filename),fontsize) )
01330         fontnumber+=1
01331 
01332 #############################################################
01333 # Creates an info xml file from details in the job file or from the DB
01334 
01335 def getFileInformation(file, folder):
01336     outputfile = os.path.join(folder, "info.xml")
01337     impl = xml.dom.minidom.getDOMImplementation()
01338     infoDOM = impl.createDocument(None, "fileinfo", None)
01339     top_element = infoDOM.documentElement
01340 
01341     data = OrdDict((('chanid',''),
01342                     ('type',''),            ('filename',''),
01343                     ('title',''),           ('recordingdate',''),
01344                     ('recordingtime',''),   ('subtitle',''),
01345                     ('description',''),     ('rating',''),
01346                     ('coverfile',''),       ('cutlist','')))
01347 
01348     # if the jobfile has amended file details use them
01349     details = file.getElementsByTagName("details")
01350     if details.length > 0:
01351         data.type =             file.attributes["type"].value
01352         data.filename =         file.attributes["filename"].value
01353         data.title =            details[0].attributes["title"].value
01354         data.recordingdate =    details[0].attributes["startdate"].value
01355         data.recordingtime =    details[0].attributes["starttime"].value
01356         data.subtitle =         details[0].attributes["subtitle"].value
01357         data.description =      getText(details[0])
01358 
01359         # if this a myth recording we still need to find the chanid, starttime and hascutlist
01360         if file.attributes["type"].value=="recording":
01361             filename = file.attributes["filename"].value
01362             try:
01363                 rec = DB.searchRecorded(basename=os.path.basename(filename)).next()
01364             except StopIteration:
01365                 fatalError("Failed to get recording details from the DB for %s" % filename)
01366 
01367             data.chanid = rec.chanid
01368             data.recordingtime = rec.starttime.isoformat()
01369             data.recordingdate = rec.starttime.isoformat()
01370 
01371             cutlist = rec.markup.getcutlist()
01372             if len(cutlist):
01373                 data.hascutlist = 'yes'
01374                 if file.attributes["usecutlist"].value == "0" and addCutlistChapters == True:
01375                     chapterlist = ['00:00:00']
01376                     res, fps, ar = getVideoParams(folder)
01377                     for s,e in cutlist:
01378                         chapterlist.append(frameToTime(s, float(fps)))
01379                     data.chapterlist = ','.join(chapterlist)
01380             else:
01381                 data.hascutlist = 'no'
01382 
01383     elif file.attributes["type"].value=="recording":
01384         filename = file.attributes["filename"].value
01385         try:
01386             rec = DB.searchRecorded(basename=os.path.basename(filename)).next()
01387         except StopIteration:
01388             fatalError("Failed to get recording details from the DB for %s" % filename)
01389 
01390         write("          " + rec.title)
01391         data.type           = file.attributes["type"].value
01392         data.filename       = filename
01393         data.title          = rec.title
01394         data.recordingdate  = rec.progstart.strftime(dateformat)
01395         data.recordingtime  = rec.progstart.strftime(timeformat)
01396         data.subtitle       = rec.subtitle
01397         data.description    = rec.description
01398         data.rating         = str(rec.stars)
01399         data.chanid         = rec.chanid
01400         data.starttime      = rec.starttime.isoformat()
01401 
01402         cutlist = rec.markup.getcutlist()
01403         if len(cutlist):
01404             data.hascutlist = 'yes'
01405             if file.attributes["usecutlist"].value == "0" and addCutlistChapters == True:
01406                 chapterlist = ['00:00:00']
01407                 res, fps, ar = getVideoParams(folder)
01408                 for s,e in cutlist:
01409                     chapterlist.append(frameToTime(s, float(fps)))
01410                 data.chapterlist = ','.join(chapterlist)
01411         else:
01412             data.hascutlist = 'no'
01413 
01414     elif file.attributes["type"].value=="video":
01415         filename = file.attributes["filename"].value
01416         try:
01417             vid = MVID.searchVideos(file=filename).next()
01418         except StopIteration:
01419             vid = Video.fromFilename(filename)
01420 
01421         data.type = file.attributes["type"].value
01422         data.filename = filename
01423         data.title = vid.title
01424 
01425         if vid.year != 1895:
01426             data.recordingdate = str(vid.year)
01427 
01428         data.subtitle = vid.subtitle
01429 
01430         if (vid.plot is not None) and (vid.plot != 'None'):
01431             data.description = vid.plot
01432 
01433         data.rating = str(vid.userrating)
01434 
01435         if doesFileExist(vid.coverfile):
01436             data.coverfile = vid.coverfile
01437 
01438     elif file.attributes["type"].value=="file":
01439         data.type =         file.attributes["type"].value
01440         data.filename =     file.attributes["filename"].value
01441         data.title =        file.attributes["filename"].value
01442 
01443     # if the jobfile has thumb image details copy the images to the work dir
01444     thumbs = file.getElementsByTagName("thumbimages")
01445     if thumbs.length > 0:
01446         thumbs = thumbs[0]
01447         thumbs = file.getElementsByTagName("thumb")
01448         thumblist = []
01449         res, fps, ar = getVideoParams(folder)
01450 
01451         for thumb in thumbs:
01452             caption = thumb.attributes["caption"].value
01453             frame = thumb.attributes["frame"].value
01454             filename = thumb.attributes["filename"].value
01455             if caption != "Title":
01456                 thumblist.append(frameToTime(int(frame), float(fps)))
01457 
01458             # copy thumb file to work dir
01459             copy(filename, folder)
01460 
01461         data.thumblist = ','.join(thumblist)
01462 
01463     for k,v in data.items():
01464         write( "Node = %s, Data = %s" % (k, v))
01465         node = infoDOM.createElement(k)
01466         # v may be either an integer. Therefore we have to
01467         # convert it to an unicode-string
01468         # If it is already a string it is not encoded as all
01469         # strings in this script are unicode. As no
01470         # encoding-argument is supplied to unicode() it does
01471         # nothing in this case.
01472         node.appendChild(infoDOM.createTextNode(unicode(v)))
01473         top_element.appendChild(node)
01474 
01475     WriteXMLToFile (infoDOM, outputfile)
01476 
01477 #############################################################
01478 # Write an xml file to disc
01479 
01480 def WriteXMLToFile(myDOM, filename):
01481     #Save the XML file to disk for use later on
01482     f=open(filename, 'w')
01483     f.write(myDOM.toxml("UTF-8"))
01484     f.close()
01485 
01486 
01487 #############################################################
01488 # Pre-process a single video/recording file
01489 
01490 def preProcessFile(file, folder, count):
01491     """Pre-process a single video/recording file."""
01492 
01493     write( "Pre-processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
01494 
01495     #As part of this routine we need to pre-process the video:
01496     #1. check the file actually exists
01497     #2. extract information from mythtv for this file in xml file
01498     #3. Extract a single frame from the video to use as a thumbnail and resolution check
01499     mediafile=""
01500 
01501     if file.attributes["type"].value == "recording":
01502         mediafile = file.attributes["filename"].value
01503     elif file.attributes["type"].value == "video":
01504         mediafile = os.path.join(videopath, file.attributes["filename"].value)
01505     elif file.attributes["type"].value == "file":
01506         mediafile = file.attributes["filename"].value
01507     else:
01508         fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
01509 
01510     if doesFileExist(mediafile) == False:
01511         fatalError("Source file does not exist: " + mediafile)
01512 
01513     if file.hasAttribute("localfilename"):
01514         mediafile = file.attributes["localfilename"].value
01515 
01516     getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
01517     copy(os.path.join(folder, "streaminfo.xml"), os.path.join(folder, "streaminfo_orig.xml"))
01518 
01519     getFileInformation(file, folder)
01520 
01521     videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
01522 
01523     write( "Video resolution is %s by %s" % (videosize[0], videosize[1]))
01524 
01525 #############################################################
01526 # Re-encodes an audio stream to ac3
01527 
01528 def encodeAudio(format, sourcefile, destinationfile, deletesourceafterencode):
01529     write( "Encoding audio to "+format)
01530     if format == "ac3":
01531         cmd = "mythffmpeg -v 0 -y "
01532 
01533         if cpuCount > 1:
01534             cmd += "-threads %d " % cpuCount
01535 
01536         cmd += "-i %s -f ac3 -ab 192k -ar 48000 %s" % (quoteCmdArg(sourcefile), quoteCmdArg(destinationfile))
01537         result = runCommand(cmd)
01538 
01539         if result != 0:
01540             fatalError("Failed while running mythffmpeg to re-encode the audio to ac3\n"
01541                        "Command was %s" % cmd)
01542     else:
01543         fatalError("Unknown encodeAudio format " + format)
01544 
01545     if deletesourceafterencode==True:
01546         os.remove(sourcefile)
01547 
01548 #############################################################
01549 # Recombines a video and one or two audio streams back together
01550 # adding in the NAV packets required to create a DVD
01551 
01552 def multiplexMPEGStream(video, audio1, audio2, destination, syncOffset):
01553     """multiplex one video and one or two audio streams together"""
01554 
01555     write("Multiplexing MPEG stream to %s" % destination)
01556 
01557     # no need to use a sync offset if projectx was used to demux the streams 
01558     if useprojectx:
01559         syncOffset = 0
01560     else:
01561         if useSyncOffset == True:
01562             write("Adding sync offset of %dms" % syncOffset)
01563         else:
01564             write("Using sync offset is disabled - it would be %dms" % syncOffset)
01565             syncOffset = 0
01566 
01567     if doesFileExist(destination)==True:
01568         os.remove(destination)
01569 
01570     # figure out what audio files to use
01571     if doesFileExist(audio1 + ".ac3"):
01572         audio1 = audio1 + ".ac3"
01573     elif doesFileExist(audio1 + ".mp2"):
01574         audio1 = audio1 + ".mp2"
01575     else:
01576         fatalError("No audio stream available!")
01577 
01578     if doesFileExist(audio2 + ".ac3"):
01579         audio2 = audio2 + ".ac3"
01580     elif doesFileExist(audio2 + ".mp2"):
01581         audio2 = audio2 + ".mp2"
01582 
01583     # if subtitles exist, we need to run sequentially, so they can be
01584     # multiplexed to the final file
01585     if os.path.exists(os.path.dirname(destination) + "/stream.d/spumux.xml"):
01586         localUseFIFO=False
01587     else:
01588         localUseFIFO=useFIFO
01589 
01590     if localUseFIFO==True:
01591         os.mkfifo(destination)
01592         mode=os.P_NOWAIT
01593     else:
01594         mode=os.P_WAIT
01595 
01596     checkCancelFlag()
01597 
01598     if not doesFileExist(audio2):
01599         write("Available streams - video and one audio stream")
01600         result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
01601                     '-M',
01602                     '-f', '8',
01603                     '-v', '0',
01604                     '--sync-offset', '%sms' % syncOffset,
01605                     '-o', destination,
01606                     video,
01607                     audio1)
01608     else:
01609         write("Available streams - video and two audio streams")
01610         result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
01611                     '-M',
01612                     '-f', '8',
01613                     '-v', '0',
01614                     '--sync-offset', '%sms' % syncOffset,
01615                     '-o', destination,
01616                     video,
01617                     audio1,
01618                     audio2)
01619 
01620     if localUseFIFO == True:
01621         write( "Multiplex started PID=%s" % result)
01622         return result
01623     else:
01624         if result != 0:
01625             fatalError("mplex failed with result %d" % result)
01626 
01627     # run spumux to add subtitles if they exist
01628     if os.path.exists(os.path.dirname(destination) + "/stream.d/spumux.xml"):
01629         write("Checking integrity of subtitle pngs")
01630         command = quoteCmdArg(os.path.join(scriptpath, "testsubtitlepngs.sh")) + " " + quoteCmdArg(os.path.dirname(destination) + "/stream.d/spumux.xml")
01631         result = runCommand(command)
01632         if result<>0:
01633             fatalError("Failed while running testsubtitlepngs.sh - %s" % command)
01634 
01635         write("Running spumux to add subtitles")
01636         command = quoteCmdArg(path_spumux[0]) + " -P %s <%s >%s" % (quoteCmdArg(os.path.dirname(destination) + "/stream.d/spumux.xml"), quoteCmdArg(destination), quoteCmdArg(os.path.splitext(destination)[0] + "-sub.mpg"))
01637         result = runCommand(command)
01638         if result<>0:
01639             nonfatalError("Failed while running spumux.\n"
01640                           "Command was - %s.\n"
01641                           "Look in the full log to see why it failed" % command)
01642             os.remove(os.path.splitext(destination)[0] + "-sub.mpg")
01643         else:
01644             os.rename(os.path.splitext(destination)[0] + "-sub.mpg", destination)
01645 
01646     return True
01647 
01648 
01649 #############################################################
01650 # Creates a stream xml file for a video file
01651 
01652 def getStreamInformation(filename, xmlFilename, lenMethod):
01653     """create a stream.xml file for filename"""
01654 
01655     command = "mytharchivehelper -q -q --getfileinfo --infile %s --outfile %s --method %d" % (quoteCmdArg(filename), quoteCmdArg(xmlFilename), lenMethod)
01656 
01657 
01658     result = runCommand(command)
01659 
01660     if result <> 0:
01661         fatalError("Failed while running mytharchivehelper to get stream information.\n"
01662                    "Result: %d, Command was %s" % (result, command))
01663 
01664     # print out the streaminfo.xml file to the log
01665     infoDOM = xml.dom.minidom.parse(xmlFilename)
01666     write(xmlFilename + ":-\n" + infoDOM.toprettyxml("    ", ""), False)
01667 
01668 #############################################################
01669 # Gets the video width and height from a file's stream xml file
01670 
01671 def getVideoSize(xmlFilename):
01672     """Get video width and height from stream.xml file"""
01673 
01674     #open the XML containing information about this file
01675     infoDOM = xml.dom.minidom.parse(xmlFilename)
01676     #error out if its the wrong XML
01677 
01678     if infoDOM.documentElement.tagName != "file":
01679         fatalError("This info file doesn't look right (%s)." % xmlFilename)
01680     nodes = infoDOM.getElementsByTagName("video")
01681     if nodes.length == 0:
01682         fatalError("Didn't find any video elements in stream info file. (%s)" % xmlFilename)
01683 
01684     if nodes.length > 1:
01685         write("Found more than one video element in stream info file.!!!")
01686     node = nodes[0]
01687     width = int(node.attributes["width"].value)
01688     height = int(node.attributes["height"].value)
01689 
01690     return (width, height)
01691 
01692 #############################################################
01693 # Run a file though the lossless encoder optionally removing commercials
01694 
01695 def runMythtranscode(chanid, starttime, destination, usecutlist, localfile):
01696     """Use mythtranscode to cut commercials and/or clean up an mpeg2 file"""
01697 
01698     rec = DB.searchRecorded(chanid=chanid, starttime=starttime).next()
01699     cutlist = rec.markup.getcutlist()
01700 
01701     cutlist_s = ""
01702     if usecutlist and len(cutlist):
01703         cutlist_s = "'"
01704         for cut in cutlist:
01705             cutlist_s += ' %d-%d ' % cut
01706         cutlist_s += "'"
01707         write("Using cutlist: %s" % cutlist_s)
01708 
01709     if (localfile != ""):
01710         localfile = quoteFilename(localfile)
01711         if usecutlist == True:
01712             command = "mythtranscode --mpeg2 --honorcutlist %s --infile %s --outfile %s" % (cutlist_s, quoteCmdArg(localfile), quoteCmdArg(destination))
01713         else:
01714             command = "mythtranscode --mpeg2 --infile %s --outfile %s" % (quoteCmdArg(localfile), quoteCmdArg(destination))
01715     else:
01716         if usecutlist == True:
01717             command = "mythtranscode --mpeg2 --honorcutlist --chanid %s --starttime %s --outfile %s" % (chanid, starttime, quoteCmdArg(destination))
01718         else:
01719             command = "mythtranscode --mpeg2 --chanid %s --starttime %s --outfile %s" % (chanid, starttime, quoteCmdArg(destination))
01720 
01721     result = runCommand(command)
01722 
01723     if (result != 0):
01724         write("Failed while running mythtranscode to cut commercials and/or clean up an mpeg2 file.\n"
01725               "Result: %d, Command was %s" % (result, command))
01726         return False;
01727 
01728     return True
01729 
01730 
01731 #############################################################
01732 # Create a projectX cut list for a recording
01733 
01734 def generateProjectXCutlist(chanid, starttime, folder):
01735     """generate cutlist_x.txt for ProjectX"""
01736 
01737     rec = DB.searchRecorded(chanid=chanid, starttime=starttime).next()
01738     cutlist = rec.markup.getcutlist()
01739 
01740     if len(cutlist):
01741         with open(os.path.join(folder, "cutlist_x.txt"), 'w') as cutlist_f:
01742             cutlist_f.write("CollectionPanel.CutMode=2\n")
01743             i = 0
01744             for cut in cutlist:
01745                 # we need to reverse the cutlist because ProjectX wants to know
01746                 # the bits to keep not what to cut
01747                 
01748                 if i == 0:
01749                     if cut[0] != 0:
01750                         cutlist_f.write('0\n%d\n' % cut[0])
01751                     cutlist_f.write('%d\n' % cut[1])
01752                 elif i == len(cutlist) - 1:
01753                     cutlist_f.write('%d\n' % cut[0])
01754                     if cut[1] != 9999999:
01755                         cutlist_f.write('%d\n9999999\n' % cut[1])
01756                 else:
01757                     cutlist_f.write('%d\n%d\n' % cut)
01758 
01759                 i+=1
01760         return True
01761     else:
01762         write("No cutlist in the DB for chanid %s, starttime %s" % chanid, starttime)
01763         return False
01764 
01765 #############################################################
01766 # Use Project-X to cut commercials and/or demux an mpeg2 file
01767 
01768 def runProjectX(chanid, starttime, folder, usecutlist, file):
01769     """Use Project-X to cut commercials and demux an mpeg2 file"""
01770 
01771     if usecutlist:
01772         if generateProjectXCutlist(chanid, starttime, folder) == False:
01773             write("Failed to generate Project-X cutlist.")
01774             return False
01775 
01776     if os.path.exists(file) != True:
01777         write("Error: input file doesn't exist on local filesystem")
01778         return False
01779 
01780     command = quoteCmdArg(path_projectx[0]) + " %s -id '%s' -set ExternPanel.appendPidToFileName=1 -out %s -name stream" % (quoteCmdArg(file), getStreamList(folder), quoteCmdArg(folder))
01781     if usecutlist == True:
01782         command += " -cut %s" % quoteCmdArg(os.path.join(folder, "cutlist_x.txt"))
01783     write(command)
01784     result = runCommand(command)
01785 
01786     if (result != 0):
01787         write("Failed while running Project-X to cut commercials and/or demux an mpeg2 file.\n"
01788               "Result: %d, Command was %s" % (result, command))
01789         return False;
01790 
01791 
01792     # workout which files we need and rename them
01793     video, audio1, audio2 = selectStreams(folder)
01794     if addSubtitles:
01795         subtitles = selectSubtitleStream(folder)
01796 
01797     videoID_hex = "0x%x" % video[VIDEO_ID]
01798     if audio1[AUDIO_ID] != -1:
01799         audio1ID_hex = "0x%x" % audio1[AUDIO_ID]
01800     else:
01801         audio1ID_hex = ""
01802     if audio2[AUDIO_ID] != -1:
01803         audio2ID_hex = "0x%x" % audio2[AUDIO_ID]
01804     else:
01805         audio2ID_hex = ""
01806     if addSubtitles and subtitles[SUBTITLE_ID] != -1:
01807         subtitlesID_hex = "0x%x" % subtitles[SUBTITLE_ID]
01808     else:
01809         subtitlesID_hex = ""
01810 
01811 
01812     files = os.listdir(folder)
01813     for file in files:
01814         if file[0:9] == "stream{0x": # don't rename files that have already been renamed
01815             PID = file[7:13]
01816             SubID = file[19:23]
01817             if PID == videoID_hex or SubID == videoID_hex:
01818                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.mv2"))
01819             elif PID == audio1ID_hex or SubID == audio1ID_hex:
01820                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0." + file[-3:]))
01821             elif PID == audio2ID_hex or SubID == audio2ID_hex:
01822                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1." + file[-3:]))
01823             elif PID == subtitlesID_hex or SubID == subtitlesID_hex:
01824                 if file[-3:] == "sup":
01825                     os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup"))
01826                 else:
01827                     os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup.IFO"))
01828 
01829 
01830     # Fallback if assignment and renaming by ID failed
01831 
01832     files = os.listdir(folder)
01833     for file in files:
01834         if file[0:9] == "stream{0x": # don't rename files that have already been renamed
01835             if not os.path.exists(os.path.join(folder, "stream.mv2")) and file[-3:] == "m2v":
01836                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.mv2"))
01837             elif not (os.path.exists(os.path.join(folder, "stream0.ac3")) or os.path.exists(os.path.join(folder, "stream0.mp2"))) and file[-3:] == "ac3":
01838                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0.ac3"))
01839             elif not (os.path.exists(os.path.join(folder, "stream0.ac3")) or os.path.exists(os.path.join(folder, "stream0.mp2"))) and file[-3:] == "mp2":
01840                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0.mp2"))
01841             elif not (os.path.exists(os.path.join(folder, "stream1.ac3")) or os.path.exists(os.path.join(folder, "stream1.mp2"))) and file[-3:] == "ac3":
01842                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1.ac3"))
01843             elif not (os.path.exists(os.path.join(folder, "stream1.ac3")) or os.path.exists(os.path.join(folder, "stream1.mp2"))) and file[-3:] == "mp2":
01844                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1.mp2"))
01845             elif not os.path.exists(os.path.join(folder, "stream.sup")) and file[-3:] == "sup":
01846                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup"))
01847             elif not os.path.exists(os.path.join(folder, "stream.sup.IFO")) and file[-3:] == "IFO":
01848                 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup.IFO"))
01849 
01850 
01851     # if we have some dvb subtitles and the user wants to add them to the DVD
01852     # convert them to pngs and create the spumux xml file
01853     if addSubtitles:
01854         if (os.path.exists(os.path.join(folder, "stream.sup")) and
01855             os.path.exists(os.path.join(folder, "stream.sup.IFO"))):
01856             write("Found DVB subtitles converting to DVD subtitles")
01857             command = "mytharchivehelper -q -q --sup2dast "
01858             command += " --infile %s --ifofile %s --delay 0" % (quoteCmdArg(os.path.join(folder, "stream.sup")), quoteCmdArg(os.path.join(folder, "stream.sup.IFO")))
01859 
01860             result = runCommand(command)
01861 
01862             if result != 0:
01863                 write("Failed while running mytharchivehelper to convert DVB subtitles to DVD subtitles.\n"
01864                       "Result: %d, Command was %s" % (result, command))
01865                 return False
01866 
01867             # sanity check the created spumux.xml
01868             checkSubtitles(os.path.join(folder, "stream.d", "spumux.xml"))
01869 
01870     return True
01871 
01872 #############################################################
01873 # convert time stamp to pts
01874 
01875 def ts2pts(time):
01876     h = int(time[0:2]) * 3600 * 90000
01877     m = int(time[3:5]) * 60 * 90000
01878     s = int(time[6:8]) * 90000
01879     ms = int(time[9:11]) * 90
01880 
01881     return h + m + s + ms
01882 
01883 #############################################################
01884 # check the given spumux.xml file for consistancy
01885 
01886 def checkSubtitles(spumuxFile):
01887 
01888     #open the XML containing information about this file
01889     subDOM = xml.dom.minidom.parse(spumuxFile)
01890 
01891     #error out if its the wrong XML
01892     if subDOM.documentElement.tagName != "subpictures":
01893         fatalError("This does not look like a spumux.xml file (%s)" % spumuxFile)
01894 
01895     streamNodes = subDOM.getElementsByTagName("stream")
01896     if streamNodes.length == 0:
01897         write("Didn't find any stream elements in file.!!!")
01898         return
01899     streamNode = streamNodes[0]
01900 
01901     nodes = subDOM.getElementsByTagName("spu")
01902     if nodes.length == 0:
01903         write("Didn't find any spu elements in file.!!!")
01904         return
01905 
01906     lastStart = -1
01907     lastEnd = -1
01908     for node in nodes:
01909         errored = False
01910         start = ts2pts(node.attributes["start"].value)
01911         end = ts2pts(node.attributes["end"].value)
01912         image = node.attributes["image"].value
01913 
01914         if end <= start:
01915             errored = True
01916         if start <= lastEnd:
01917             errored = True
01918 
01919         if errored:
01920             write("removing subtitle: %s to %s - (%d - %d (%d))" % (node.attributes["start"].value, node.attributes["end"].value, start, end, lastEnd), False)
01921             streamNode.removeChild(node)
01922             node.unlink()
01923 
01924         lastStart = start
01925         lastEnd = end
01926 
01927     WriteXMLToFile(subDOM, spumuxFile)
01928 
01929 #############################################################
01930 # Grabs a sequence of consecutive frames from a file
01931 
01932 def extractVideoFrame(source, destination, seconds):
01933     write("Extracting thumbnail image from %s at position %s" % (source, seconds))
01934     write("Destination file %s" % destination)
01935 
01936     if doesFileExist(destination) == False:
01937 
01938         if videomode=="pal":
01939             fr=frameratePAL
01940         else:
01941             fr=framerateNTSC
01942 
01943         command = "mytharchivehelper -q -q --createthumbnail --infile %s --thumblist '%s' --outfile %s" % (quoteCmdArg(source), seconds, quoteCmdArg(destination))
01944         result = runCommand(command)
01945         if result <> 0:
01946             fatalError("Failed while running mytharchivehelper to get thumbnails.\n"
01947                        "Result: %d, Command was %s" % (result, command))
01948     try:
01949         myimage=Image.open(destination,"r")
01950 
01951         if myimage.format <> "JPEG":
01952             write( "Something went wrong with thumbnail capture - " + myimage.format)
01953             return (0L,0L)
01954         else:
01955             return myimage.size
01956     except IOError:
01957         return (0L, 0L)
01958 
01959 #############################################################
01960 # Grabs a list of single frames from a file
01961 
01962 def extractVideoFrames(source, destination, thumbList):
01963     write("Extracting thumbnail images from: %s - at %s" % (source, thumbList))
01964     write("Destination file %s" % destination)
01965 
01966     command = "mytharchivehelper -q -q --createthumbnail --infile %s --thumblist '%s' --outfile %s" % (quoteCmdArg(source), thumbList, quoteCmdArg(destination))
01967     write(command)
01968     result = runCommand(command)
01969     if result <> 0:
01970         fatalError("Failed while running mytharchivehelper to get thumbnails.\n"
01971                    "Result: %d, Command was %s" % (result, command))
01972 
01973 #############################################################
01974 # Re-encodes a file to mpeg2
01975 
01976 def encodeVideoToMPEG2(source, destvideofile, video, audio1, audio2, aspectratio, profile):
01977     """Encodes an unknown video source file eg. AVI to MPEG2 video and AC3 audio, use mythffmpeg"""
01978 
01979     profileNode = findEncodingProfile(profile)
01980 
01981     passes = int(getText(profileNode.getElementsByTagName("passes")[0]))
01982 
01983     command = "mythffmpeg"
01984 
01985     if cpuCount > 1:
01986         command += " -threads %d" % cpuCount
01987 
01988     parameters = profileNode.getElementsByTagName("parameter")
01989 
01990     for param in parameters:
01991         name = param.attributes["name"].value
01992         value = param.attributes["value"].value
01993 
01994         # do some parameter substitution
01995         if value == "%inputfile":
01996             value = quoteCmdArg(source)
01997         if value == "%outputfile":
01998             value = quoteCmdArg(destvideofile)
01999         if value == "%aspect":
02000             value = aspectratio
02001 
02002         # only re-encode the audio if it is not already in AC3 format
02003         if audio1[AUDIO_CODEC] == "AC3":
02004             if name == "-acodec":
02005                 value = "copy"
02006             if name == "-ar" or name == "-ab" or name == "-ac":
02007                 name = ""
02008                 value = ""
02009 
02010         if name != "":
02011             command += " " + name
02012 
02013         if value != "":
02014             command += " " + value
02015 
02016 
02017     #add second audio track if required
02018     if audio2[AUDIO_ID] != -1:
02019         for param in parameters:
02020             name = param.attributes["name"].value
02021             value = param.attributes["value"].value
02022 
02023             # only re-encode the audio if it is not already in AC3 format
02024             if audio1[AUDIO_CODEC] == "AC3":
02025                 if name == "-acodec":
02026                     value = "copy"
02027                 if name == "-ar" or name == "-ab" or name == "-ac":
02028                     name = ""
02029                     value = ""
02030 
02031             if name == "-acodec" or name == "-ar" or name == "-ab" or name == "-ac":
02032                     command += " " + name + " " + value
02033 
02034         command += " -newaudio" 
02035 
02036     #make sure we get the correct stream(s) that we want
02037     command += " -map 0:%d -map 0:%d " % (video[VIDEO_INDEX], audio1[AUDIO_INDEX])
02038     if audio2[AUDIO_ID] != -1:
02039         command += "-map 0:%d" % (audio2[AUDIO_INDEX])
02040 
02041     if passes == 1:
02042         write(command)
02043         result = runCommand(command)
02044         if result!=0:
02045             fatalError("Failed while running mythffmpeg to re-encode video.\n"
02046                        "Command was %s" % command)
02047 
02048     else:
02049         passLog = os.path.join(getTempPath(), 'pass')
02050 
02051         pass1 = string.replace(command, "%passno","1")
02052         pass1 = string.replace(pass1, "%passlogfile", quoteCmdArg(passLog))
02053         write("Pass 1 - " + pass1)
02054         result = runCommand(pass1)
02055 
02056         if result!=0:
02057             fatalError("Failed while running mythffmpeg (Pass 1) to re-encode video.\n"
02058                        "Command was %s" % command)
02059 
02060         if os.path.exists(destvideofile):
02061             os.remove(destvideofile)
02062 
02063         pass2 = string.replace(command, "%passno","2")
02064         pass2 = string.replace(pass2, "%passlogfile", passLog)
02065         write("Pass 2 - " + pass2)
02066         result = runCommand(pass2)
02067 
02068         if result!=0:
02069             fatalError("Failed while running mythffmpeg (Pass 2) to re-encode video.\n"
02070                        "Command was %s" % command)
02071 #############################################################
02072 # Re-encodes a nuv file to mpeg2 optionally removing commercials
02073 
02074 def encodeNuvToMPEG2(chanid, starttime, mediafile, destvideofile, folder, profile, usecutlist):
02075     """Encodes a nuv video source file to MPEG2 video and AC3 audio, using mythtranscode & mythffmpeg"""
02076 
02077     # make sure mythtranscode hasn't left some stale fifos hanging around
02078     if ((doesFileExist(os.path.join(folder, "audout")) or doesFileExist(os.path.join(folder, "vidout")))):
02079         fatalError("Something is wrong! Found one or more stale fifo's from mythtranscode\n"
02080                    "Delete the fifos in '%s' and start again" % folder)
02081 
02082     profileNode = findEncodingProfile(profile)
02083     parameters = profileNode.getElementsByTagName("parameter")
02084 
02085     # default values - will be overriden by values from the profile 
02086     outvideobitrate = "5000k"
02087     if videomode == "ntsc":
02088         outvideores = "720x480"
02089     else:
02090         outvideores = "720x576"
02091 
02092     outaudiochannels = 2
02093     outaudiobitrate = 384
02094     outaudiosamplerate = 48000
02095     outaudiocodec = "ac3"
02096     deinterlace = 0
02097     croptop = 0
02098     cropright = 0
02099     cropbottom = 0
02100     cropleft = 0
02101     qmin = 5
02102     qmax = 31
02103     qdiff = 31
02104 
02105     for param in parameters:
02106         name = param.attributes["name"].value
02107         value = param.attributes["value"].value
02108 
02109         # we only support a subset of the parameter for the moment
02110         if name == "-acodec":
02111             outaudiocodec = value
02112         if name == "-ac":
02113             outaudiochannels = value
02114         if name == "-ab":
02115             outaudiobitrate = value
02116         if name == "-ar":
02117             outaudiosamplerate = value
02118         if name == "-b":
02119             outvideobitrate = value
02120         if name == "-s":
02121             outvideores = value
02122         if name == "-deinterlace":
02123             deinterlace = 1
02124         if name == "-croptop":
02125             croptop = value
02126         if name == "-cropright":
02127             cropright = value
02128         if name == "-cropbottom":
02129             cropbottom = value
02130         if name == "-cropleft":
02131            cropleft = value
02132         if name == "-qmin":
02133            qmin = value
02134         if name == "-qmax":
02135            qmax = value
02136         if name == "-qdiff":
02137            qdiff = value
02138 
02139     if chanid != -1:
02140         if (usecutlist == True):
02141             PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
02142                         '--profile', '27',
02143                         '--chanid', chanid,
02144                         '--starttime', starttime,
02145                         '--honorcutlist',
02146                         '--fifodir', folder)
02147             write("mythtranscode started (using cut list) PID = %s" % PID)
02148         else:
02149             PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
02150                         '--profile', '27',
02151                         '--chanid', chanid,
02152                         '--starttime', starttime,
02153                         '--fifodir', folder)
02154 
02155             write("mythtranscode started PID = %s" % PID)
02156     elif mediafile != -1:
02157         PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
02158                 '--profile', '27',
02159                 '--infile', mediafile,
02160                 '--fifodir', folder)
02161         write("mythtranscode started (using file) PID = %s" % PID)
02162     else:
02163         fatalError("no video source passed to encodeNuvToMPEG2.\n")
02164 
02165 
02166     samplerate, channels = getAudioParams(folder)
02167     videores, fps, aspectratio = getVideoParams(folder)
02168 
02169     command =  "mythffmpeg -y "
02170 
02171     if cpuCount > 1:
02172         command += "-threads %d " % cpuCount
02173 
02174     command += "-f s16le -ar %s -ac %s -i %s " % (samplerate, channels, quoteCmdArg(os.path.join(folder, "audout"))) 
02175     command += "-f rawvideo -pix_fmt yuv420p -s %s -aspect %s -r %s " % (videores, aspectratio, fps)
02176     command += "-i %s " % quoteCmdArg(os.path.join(folder, "vidout"))
02177     command += "-aspect %s -r %s " % (aspectratio, fps)
02178     if (deinterlace == 1):
02179         command += "-deinterlace "
02180     command += "-croptop %s -cropright %s -cropbottom %s -cropleft %s " % (croptop, cropright, cropbottom, cropleft)
02181     command += "-s %s -b %s -vcodec mpeg2video " % (outvideores, outvideobitrate)
02182     command += "-qmin %s -qmax %s -qdiff %s " % (qmin, qmax, qdiff)
02183     command += "-ab %s -ar %s -acodec %s " % (outaudiobitrate, outaudiosamplerate, outaudiocodec)
02184     command += "-f dvd %s" % quoteCmdArg(destvideofile)
02185 
02186     #wait for mythtranscode to create the fifos
02187     tries = 30
02188     while (tries and not(doesFileExist(os.path.join(folder, "audout")) and
02189                          doesFileExist(os.path.join(folder, "vidout")))):
02190         tries -= 1
02191         write("Waiting for mythtranscode to create the fifos")
02192         time.sleep(1)
02193 
02194     if (not(doesFileExist(os.path.join(folder, "audout")) and doesFileExist(os.path.join(folder, "vidout")))):
02195         fatalError("Waited too long for mythtranscode to create the fifos - giving up!!")
02196 
02197     write("Running mythffmpeg")
02198     result = runCommand(command)
02199     if result != 0:
02200         os.kill(PID, signal.SIGKILL)
02201         fatalError("Failed while running mythffmpeg to re-encode video.\n"
02202                    "Command was %s" % command)
02203 
02204 #############################################################
02205 # Runs DVDAuthor to create a DVD file structure
02206 
02207 def runDVDAuthor():
02208     write( "Starting dvdauthor")
02209     checkCancelFlag()
02210     result=os.spawnlp(os.P_WAIT, path_dvdauthor[0],path_dvdauthor[1],'-x',os.path.join(getTempPath(),'dvdauthor.xml'))
02211     if result<>0:
02212         fatalError("Failed while running dvdauthor. Result: %d" % result)
02213     write( "Finished  dvdauthor")
02214 
02215 #############################################################
02216 # Creates an ISO image from the contents of a directory
02217 
02218 def CreateDVDISO(title):
02219     write("Creating ISO image")
02220     checkCancelFlag()
02221     command = quoteCmdArg(path_mkisofs[0]) + ' -dvd-video '
02222     command += ' -V ' + quoteCmdArg(title)
02223     command += ' -o ' + quoteCmdArg(os.path.join(getTempPath(), 'mythburn.iso'))
02224     command += " " + quoteCmdArg(os.path.join(getTempPath(),'dvd'))
02225 
02226     result = runCommand(command)
02227 
02228     if result<>0:
02229         fatalError("Failed while running mkisofs.\n"
02230                    "Command was %s" % command)
02231 
02232     write("Finished creating ISO image")
02233 
02234 #############################################################
02235 # Burns the contents of a directory to create a DVD 
02236 
02237 def BurnDVDISO(title):
02238     write( "Burning ISO image to %s" % dvddrivepath)
02239 
02240     #######################
02241     # some helper functions
02242     def drivestatus():
02243         f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
02244         status = ioctl(f,CDROM.CDROM_DRIVE_STATUS, 0)
02245         os.close(f)
02246         return status
02247     def displayneededdisktype():
02248         if mediatype == DVD_SL:
02249           write("Please insert an empty single-layer disc (DVD+R or DVD-R).")
02250         if mediatype == DVD_DL:
02251           write("Please insert an empty double-layer disc (DVD+R DL or DVD-R DL).")
02252         if mediatype == DVD_RW:
02253           write("Please insert a rewritable disc (DVD+RW or DVD-RW).")
02254     def tray(action):
02255         runCommand("pumount " + quoteCmdArg(dvddrivepath));
02256         waitForDrive()
02257         try:
02258             f = os.open(drivepath, os.O_RDONLY | os.O_NONBLOCK)
02259             r = ioctl(f,action, 0)
02260             os.close(f)
02261             return True
02262         except:
02263             write("Failed to eject the disc!")
02264             return False
02265         finally:
02266             os.close(f)
02267     def waitForDrive():
02268         tries = 0
02269         while drivestatus() == CDROM.CDS_DRIVE_NOT_READY:
02270             checkCancelFlag()
02271             write("Waiting for drive")
02272             time.sleep(5)
02273             tries += 1
02274             if tries > 10:
02275                 # Try a hard reset if the device is still not ready
02276                 write("Try a hard-reset of the device")
02277                 tray(CDROM.CDROMRESET)
02278                 tries = 0
02279 
02280 
02281     ####################
02282     # start working here
02283 
02284 
02285     finished = False
02286     while not finished:
02287         # Maybe the user has no appropriate medium or something alike. Give her the chance to cancel.
02288         checkCancelFlag()
02289 
02290         # If drive needs some time (for example to close the tray) give it to it
02291         waitForDrive()
02292 
02293         if drivestatus() == CDROM.CDS_DISC_OK or drivestatus() == CDROM.CDS_NO_INFO:
02294 
02295             # If the frontend has a previously burnt DVD+RW mounted,
02296             # growisofs will fail to burn it, so try to pumount it first...
02297             runCommand("pumount " + quoteCmdArg(dvddrivepath));
02298 
02299 
02300             command = quoteCmdArg(path_growisofs[0]) + " -dvd-compat"
02301             if drivespeed != 0:
02302                 command += " -speed=%d" % drivespeed
02303             if mediatype == DVD_RW and erasedvdrw == True:
02304                 command += " -use-the-force-luke"
02305             command += " -Z " + quoteCmdArg(dvddrivepath) + " -dvd-video -V " + quoteCmdArg(title) + " " + quoteCmdArg(os.path.join(getTempPath(),'dvd'))
02306             write(command)
02307             write("Running growisofs to burn DVD")
02308 
02309             result = runCommand(command)
02310             if result == 0:
02311                 finished = True
02312             else:
02313                 if result == 252:
02314                     write("-"*60)
02315                     write("You probably inserted a medium of wrong type.")
02316                 elif (result == 156):
02317                     write("-"*60)
02318                     write("You probably inserted a non-empty, corrupt or too small medium.")
02319                 elif (result == 144):
02320                     write("-"*60)
02321                     write("You inserted a non-empty medium.")
02322                 else:
02323                     write("-"*60)
02324                     write("ERROR: Failed while running growisofs.")
02325                     write("Result %d, Command was: %s" % (result, command))
02326                     write("Please check mythburn.log for further information")
02327                     write("-"*60)
02328                     write("")
02329                     write("Going to try it again until canceled by user:")
02330                     write("-"*60)
02331                     write("")
02332                 displayneededdisktype()
02333 
02334             # eject the disc
02335             tray(CDROM.CDROMEJECT)
02336 
02337 
02338         elif drivestatus() == CDROM.CDS_TRAY_OPEN:
02339             displayneededdisktype()
02340             write("Waiting for tray to close.")
02341             # Wait until user closes tray or cancels
02342             while drivestatus() == CDROM.CDS_TRAY_OPEN:
02343                 checkCancelFlag()
02344                 time.sleep(5)
02345         elif drivestatus() == CDROM.CDS_NO_DISC:
02346             tray(CDROM.CDROMEJECT)
02347             displayneededdisktype()
02348 
02349     write("Finished burning ISO image")
02350 
02351 #############################################################
02352 # Splits a file into the separate audio and video streams
02353 # using mythreplex
02354 
02355 def deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2):
02356 
02357     if getFileType(folder) == "mpegts":
02358         command = "mythreplex --demux --fix_sync -t TS -o %s " % quoteCmdArg(folder + "/stream")
02359         command += "-v %d " % (video[VIDEO_ID])
02360 
02361         if audio1[AUDIO_ID] != -1: 
02362             if audio1[AUDIO_CODEC] == 'MP2':
02363                 command += "-a %d " % (audio1[AUDIO_ID])
02364             elif audio1[AUDIO_CODEC] == 'AC3':
02365                 command += "-c %d " % (audio1[AUDIO_ID])
02366             elif audio1[AUDIO_CODEC] == 'EAC3':
02367                 command += "-c %d " % (audio1[AUDIO_ID])
02368 
02369         if audio2[AUDIO_ID] != -1: 
02370             if audio2[AUDIO_CODEC] == 'MP2':
02371                 command += "-a %d " % (audio2[AUDIO_ID])
02372             elif audio2[AUDIO_CODEC] == 'AC3':
02373                 command += "-c %d " % (audio2[AUDIO_ID])
02374             elif audio2[AUDIO_CODEC] == 'EAC3':
02375                 command += "-c %d " % (audio2[AUDIO_ID])
02376 
02377     else:
02378         command = "mythreplex --demux --fix_sync -o %s " % quoteCmdArg(folder + "/stream")
02379         command += "-v %d " % (video[VIDEO_ID] & 255)
02380 
02381         if audio1[AUDIO_ID] != -1: 
02382             if audio1[AUDIO_CODEC] == 'MP2':
02383                 command += "-a %d " % (audio1[AUDIO_ID] & 255)
02384             elif audio1[AUDIO_CODEC] == 'AC3':
02385                 command += "-c %d " % (audio1[AUDIO_ID] & 255)
02386             elif audio1[AUDIO_CODEC] == 'EAC3':
02387                 command += "-c %d " % (audio1[AUDIO_ID] & 255)
02388 
02389 
02390         if audio2[AUDIO_ID] != -1: 
02391             if audio2[AUDIO_CODEC] == 'MP2':
02392                 command += "-a %d " % (audio2[AUDIO_ID] & 255)
02393             elif audio2[AUDIO_CODEC] == 'AC3':
02394                 command += "-c %d " % (audio2[AUDIO_ID] & 255)
02395             elif audio2[AUDIO_CODEC] == 'EAC3':
02396                 command += "-c %d " % (audio2[AUDIO_ID] & 255)
02397 
02398     mediafile = quoteCmdArg(mediafile)
02399     command += mediafile
02400     write("Running: " + command)
02401 
02402     result = runCommand(command)
02403 
02404     if result<>0:
02405         fatalError("Failed while running mythreplex. Command was %s" % command)
02406 
02407 #############################################################
02408 # Run M2VRequantiser
02409 
02410 def runM2VRequantiser(source,destination,factor):
02411     mega=1024.0*1024.0
02412     M2Vsize0 = os.path.getsize(source)
02413     write("Initial M2Vsize is %.2f Mb , target is %.2f Mb" % ( (float(M2Vsize0)/mega), (float(M2Vsize0)/(factor*mega)) ))
02414 
02415     command = quoteCmdArg(path_M2VRequantiser[0])
02416     command += " %.5f "  % factor
02417     command += " %s "  % M2Vsize0
02418     command += " <  %s " % quoteCmdArg(source)
02419     command += " >  %s " % quoteCmdArg(destination)
02420  
02421     write("Running: " + command)
02422     result = runCommand(command)
02423     if result<>0:
02424         fatalError("Failed while running M2VRequantiser. Command was %s" % command)
02425 
02426     M2Vsize1 = os.path.getsize(destination)
02427        
02428     write("M2Vsize after requant is  %.2f Mb " % (float(M2Vsize1)/mega))
02429     fac1=float(M2Vsize0) / float(M2Vsize1)
02430     write("Factor demanded %.5f, achieved %.5f, ratio %.5f " % ( factor, fac1, fac1/factor))
02431 
02432 #############################################################
02433 # Calculates the total size of all the video, audio and menu files 
02434 
02435 def calculateFileSizes(files):
02436     """ Returns the sizes of all video, audio and menu files"""
02437     filecount=0
02438     totalvideosize=0
02439     totalaudiosize=0
02440     totalmenusize=0
02441 
02442     for node in files:
02443         filecount+=1
02444         #Generate a temp folder name for this file
02445         folder=getItemTempPath(filecount)
02446         #Process this file
02447         file=os.path.join(folder,"stream.mv2")
02448         #Get size of vobfile in MBytes
02449         totalvideosize+=os.path.getsize(file) 
02450 
02451         #Get size of audio track 1
02452         if doesFileExist(os.path.join(folder,"stream0.ac3")):
02453             totalaudiosize+=os.path.getsize(os.path.join(folder,"stream0.ac3")) 
02454         if doesFileExist(os.path.join(folder,"stream0.mp2")):
02455             totalaudiosize+=os.path.getsize(os.path.join(folder,"stream0.mp2")) 
02456 
02457         #Get size of audio track 2 if available 
02458         if doesFileExist(os.path.join(folder,"stream1.ac3")):
02459             totalaudiosize+=os.path.getsize(os.path.join(folder,"stream1.ac3")) 
02460         if doesFileExist(os.path.join(folder,"stream1.mp2")):
02461             totalaudiosize+=os.path.getsize(os.path.join(folder,"stream1.mp2")) 
02462 
02463         # add chapter menu if available
02464         if doesFileExist(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount)):
02465             totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount)) 
02466 
02467         # add details page if available
02468         if doesFileExist(os.path.join(getTempPath(),"details-%s.mpg" % filecount)):
02469             totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"details-%s.mpg" % filecount))
02470 
02471     filecount=1
02472     while doesFileExist(os.path.join(getTempPath(),"menu-%s.mpg" % filecount)):
02473         totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"menu-%s.mpg" % filecount))
02474         filecount+=1
02475 
02476     return totalvideosize,totalaudiosize,totalmenusize
02477 
02478 ########################################
02479 #returns total size of bitrate-limited m2v files
02480 
02481 def total_mv2_brl(files,rate): 
02482     tvsize=0  
02483     filecount=0
02484     for node in files:
02485         filecount+=1
02486         folder=getItemTempPath(filecount)
02487         progduration=getLengthOfVideo(filecount)
02488         file=os.path.join(folder,"stream.mv2")
02489         progvsize=os.path.getsize(file)
02490         progvbitrate=progvsize/progduration
02491         if progvbitrate>rate : 
02492             tvsize+=progduration*rate
02493         else:
02494             tvsize+=progvsize
02495 
02496     return tvsize    
02497 
02498 #########################################
02499 # Uses requantiser if available to shrink the video streams so 
02500 # they will fit on a DVD
02501 
02502 def performMPEG2Shrink(files,dvdrsize):
02503     checkCancelFlag()
02504     mega=1024.0*1024.0
02505     fudge_pack=1.04  # for mpeg packing
02506     fudge_requant=1.05 # for requant shrinkage uncertainty
02507 
02508     totalvideosize,totalaudiosize,totalmenusize=calculateFileSizes(files)
02509     allfiles=totalvideosize+totalaudiosize+totalmenusize
02510 
02511     #Report findings
02512     write( "Total video  %.2f Mb, audio %.2f Mb, menus %.2f Mb." % (totalvideosize/mega,totalaudiosize/mega,totalmenusize/mega))
02513 
02514     #Subtract the audio, menus and packaging overhead from the size of the disk (we cannot shrink this further)
02515     mv2space=((dvdrsize*mega-totalmenusize)/fudge_pack)-totalaudiosize
02516  
02517     if mv2space<0:
02518         fatalError("Audio and menu files are too big. No room for video. Giving up!")
02519 
02520     if totalvideosize>mv2space:
02521         write( "Video files are %.1f Mb too big. Need to shrink." % ((totalvideosize - mv2space)/mega) )
02522 
02523         if path_M2VRequantiser[0] == "":
02524             fatalError("M2VRequantiser is not available to resize the files.  Giving up!")
02525 
02526         vsize=0
02527         duration=0
02528         filecount=0
02529         for node in files:
02530             filecount+=1
02531             folder=getItemTempPath(filecount)
02532             file=os.path.join(folder,"stream.mv2")
02533             vsize+=os.path.getsize(file)
02534             duration+=getLengthOfVideo(filecount)
02535 
02536         #We need to shrink the video files to fit into the space available.  It seems sensible 
02537         #to do this by imposing a common upper limit on the mean video bit-rate of each recording; 
02538         #this will not further reduce the visual quality of any that were transmitted at lower bit-rates.
02539 
02540         #Now find the bit-rate limit by iteration between initially defined upper and lower bounds. 
02541         #The code is based on 'rtbis' from Numerical Recipes by W H Press et al., CUP.
02542         
02543         #A small multiple of the average input bit-rate should be ok as the initial upper bound,
02544         #(although a fixed value or one related to the max value could be used), and zero as the lower bound.
02545         #The function relating bit-rate upper limit to total file size is smooth and monotonic,
02546         #so there should be no convergence problem. 
02547      
02548         vrLo=0.0
02549         vrHi=3.0*float(vsize)/duration
02550         
02551         vrate=vrLo
02552         vrinc=vrHi-vrLo
02553         count=0
02554 
02555         while count<30 :
02556             count+=1
02557             vrinc=vrinc*0.5
02558             vrtest=vrate+vrinc
02559             testsize=total_mv2_brl(files,vrtest)
02560             if (testsize<mv2space):
02561                 vrate=vrtest
02562            
02563         write("vrate %.3f kb/s, testsize %.4f , mv2space %.4f Mb " % ((vrate)/1000.0, (testsize)/mega, (mv2space)/mega) )
02564         filecount=0
02565         for node in files:
02566             filecount+=1
02567             folder=getItemTempPath(filecount)
02568             file=os.path.join(folder,"stream.mv2")
02569             progvsize=os.path.getsize(file)
02570             progduration=getLengthOfVideo(filecount)
02571             progvbitrate=progvsize/progduration
02572             write( "File %s, size %.2f Mb, rate %.2f, limit %.2f kb/s " %( filecount, float(progvsize)/mega, progvbitrate/1000.0, vrate/1000.0 ))
02573             if progvbitrate>vrate :
02574                 scalefactor=1.0+(fudge_requant*float(progvbitrate-vrate)/float(vrate))
02575                 if scalefactor>3.0 :
02576                     write( "Large shrink factor. You may not like the result! ")
02577                 runM2VRequantiser(os.path.join(getItemTempPath(filecount),"stream.mv2"),os.path.join(getItemTempPath(filecount),"stream.small.mv2"),scalefactor)
02578                 os.remove(os.path.join(getItemTempPath(filecount),"stream.mv2"))
02579                 os.rename(os.path.join(getItemTempPath(filecount),"stream.small.mv2"),os.path.join(getItemTempPath(filecount),"stream.mv2"))
02580     else:
02581         write( "Unpackaged total %.2f Mb. About %.0f Mb will be unused." % ((allfiles/mega),(mv2space-totalvideosize)/mega))
02582 
02583 #############################################################
02584 # Creates the DVDAuthor xml file used to create a standard DVD with menus
02585 
02586 def createDVDAuthorXML(screensize, numberofitems):
02587     """Creates the xml file for dvdauthor to use the MythBurn menus."""
02588 
02589     #Get the main menu node (we must only have 1)
02590     menunode=themeDOM.getElementsByTagName("menu")
02591     if menunode.length!=1:
02592         fatalError("Cannot find the menu element in the theme file")
02593     menunode=menunode[0]
02594 
02595     menuitems=menunode.getElementsByTagName("item")
02596     #Total number of video items on a single menu page (no less than 1!)
02597     itemsperpage = menuitems.length
02598     write( "Menu items per page %s" % itemsperpage)
02599     autoplaymenu = 2 + ((numberofitems + itemsperpage - 1)/itemsperpage)
02600 
02601     if wantChapterMenu:
02602         #Get the chapter menu node (we must only have 1)
02603         submenunode=themeDOM.getElementsByTagName("submenu")
02604         if submenunode.length!=1:
02605             fatalError("Cannot find the submenu element in the theme file")
02606 
02607         submenunode=submenunode[0]
02608 
02609         chapteritems=submenunode.getElementsByTagName("chapter")
02610         #Total number of video items on a single menu page (no less than 1!)
02611         chapters = chapteritems.length
02612         write( "Chapters per recording %s" % chapters)
02613 
02614         del chapteritems
02615         del submenunode
02616 
02617     #Page number counter
02618     page=1
02619 
02620     #Item counter to indicate current video item
02621     itemnum=1
02622 
02623     write( "Creating DVD XML file for dvd author")
02624 
02625     dvddom = xml.dom.minidom.parseString(
02626                 '''<dvdauthor>
02627                 <vmgm>
02628                 <menus lang="en">
02629                 <pgc entry="title">
02630                 </pgc>
02631                 </menus>
02632                 </vmgm>
02633                 </dvdauthor>''')
02634 
02635     dvdauthor_element=dvddom.documentElement
02636     menus_element = dvdauthor_element.childNodes[1].childNodes[1]
02637 
02638     dvdauthor_element.insertBefore( dvddom.createComment("""
02639     DVD Variables
02640     g0=not used
02641     g1=not used
02642     g2=title number selected on current menu page (see g4)
02643     g3=1 if intro movie has played
02644     g4=last menu page on display
02645     g5=next title to autoplay (0 or > # titles means no more autoplay)
02646     """), dvdauthor_element.firstChild )
02647     dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
02648 
02649     menus_element.appendChild( dvddom.createComment("Title menu used to hold intro movie") )
02650 
02651     dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
02652 
02653     video = dvddom.createElement("video")
02654     video.setAttribute("format",videomode)
02655 
02656     # set aspect ratio
02657     if mainmenuAspectRatio == "4:3":
02658         video.setAttribute("aspect", "4:3")
02659     else:
02660         video.setAttribute("aspect", "16:9")
02661         video.setAttribute("widescreen", "nopanscan")
02662 
02663     menus_element.appendChild(video)
02664 
02665     pgc=menus_element.childNodes[1]
02666 
02667     if wantIntro:
02668         #code to skip over intro if its already played
02669         pre = dvddom.createElement("pre")
02670         pgc.appendChild(pre)
02671         vmgm_pre_node=pre
02672         del pre
02673 
02674         node = themeDOM.getElementsByTagName("intro")[0]
02675         introFile = node.attributes["filename"].value
02676 
02677         #Pick the correct intro movie based on video format ntsc/pal
02678         vob = dvddom.createElement("vob")
02679         vob.setAttribute("pause","")
02680         vob.setAttribute("file",os.path.join(getThemeFile(themeName, videomode + '_' + introFile)))
02681         pgc.appendChild(vob)
02682         del vob
02683 
02684         #We use g3 to indicate that the intro has been played at least once
02685         #default g2 to point to first recording
02686         post = dvddom.createElement("post")
02687         post .appendChild(dvddom.createTextNode("{g3=1;g2=1;jump menu 2;}"))
02688         pgc.appendChild(post)
02689         del post
02690 
02691     while itemnum <= numberofitems:
02692         write( "Menu page %s" % page)
02693 
02694         #For each menu page we need to create a new PGC structure
02695         menupgc = dvddom.createElement("pgc")
02696         menus_element.appendChild(menupgc)
02697         menupgc.setAttribute("pause","inf")
02698 
02699         menupgc.appendChild( dvddom.createComment("Menu Page %s" % page) )
02700 
02701         #Make sure the button last highlighted is selected
02702         #g4 holds the menu page last displayed
02703         pre = dvddom.createElement("pre")
02704         pre.appendChild(dvddom.createTextNode("{button=g2*1024;g4=%s;}" % page))
02705         menupgc.appendChild(pre)    
02706 
02707         vob = dvddom.createElement("vob")
02708         vob.setAttribute("file",os.path.join(getTempPath(),"menu-%s.mpg" % page))
02709         menupgc.appendChild(vob)    
02710 
02711         #Loop menu forever
02712         post = dvddom.createElement("post")
02713         post.appendChild(dvddom.createTextNode("jump cell 1;"))
02714         menupgc.appendChild(post)
02715 
02716         #Default settings for this page
02717 
02718         #Number of video items on this menu page
02719         itemsonthispage=0
02720 
02721         endbuttons = []
02722         #Loop through all the items on this menu page
02723         while itemnum <= numberofitems and itemsonthispage < itemsperpage:
02724             menuitem=menuitems[ itemsonthispage ]
02725 
02726             itemsonthispage+=1
02727 
02728             #Get the XML containing information about this item
02729             infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(itemnum),"info.xml") )
02730             #Error out if its the wrong XML
02731             if infoDOM.documentElement.tagName != "fileinfo":
02732                 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(getItemTempPath(itemnum),"info.xml"))
02733 
02734             #write( themedom.toprettyxml())
02735 
02736             #Add this recording to this page's menu...
02737             button=dvddom.createElement("button")
02738             button.setAttribute("name","%s" % itemnum)
02739             button.appendChild(dvddom.createTextNode("{g2=" + "%s" % itemsonthispage + "; g5=0; jump title %s;}" % itemnum))
02740             menupgc.appendChild(button)
02741             del button
02742 
02743             #Create a TITLESET for each item
02744             titleset = dvddom.createElement("titleset")
02745             dvdauthor_element.appendChild(titleset)
02746 
02747             #Comment XML file with title of video
02748             comment = getText(infoDOM.getElementsByTagName("title")[0]).replace('--', '-')
02749             titleset.appendChild( dvddom.createComment(comment))
02750 
02751             menus= dvddom.createElement("menus")
02752             titleset.appendChild(menus)
02753 
02754             video = dvddom.createElement("video")
02755             video.setAttribute("format",videomode)
02756 
02757             # set the right aspect ratio
02758             if chaptermenuAspectRatio == "4:3":
02759                 video.setAttribute("aspect", "4:3")
02760             elif chaptermenuAspectRatio == "16:9":
02761                 video.setAttribute("aspect", "16:9")
02762                 video.setAttribute("widescreen", "nopanscan")
02763             else: 
02764                 # use same aspect ratio as the video
02765                 if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
02766                     video.setAttribute("aspect", "16:9")
02767                     video.setAttribute("widescreen", "nopanscan")
02768                 else:
02769                     video.setAttribute("aspect", "4:3")
02770 
02771             menus.appendChild(video)
02772 
02773             if wantChapterMenu:
02774                 mymenupgc = dvddom.createElement("pgc")
02775                 menus.appendChild(mymenupgc)
02776                 mymenupgc.setAttribute("pause","inf")
02777 
02778                 pre = dvddom.createElement("pre")
02779                 mymenupgc.appendChild(pre)
02780                 if wantDetailsPage: 
02781                     pre.appendChild(dvddom.createTextNode("{button=s7 - 1 * 1024;}"))
02782                 else:
02783                     pre.appendChild(dvddom.createTextNode("{button=s7 * 1024;}"))
02784 
02785                 vob = dvddom.createElement("vob")
02786                 vob.setAttribute("file",os.path.join(getTempPath(),"chaptermenu-%s.mpg" % itemnum))
02787                 mymenupgc.appendChild(vob)    
02788 
02789                 #Loop menu forever
02790                 post = dvddom.createElement("post")
02791                 post.appendChild(dvddom.createTextNode("jump cell 1;"))
02792                 mymenupgc.appendChild(post)
02793 
02794                 # the first chapter MUST be 00:00:00 if its not dvdauthor adds it which 
02795                 # throws of the chapter selection - so make sure we add it if needed so we
02796                 # can compensate for it in the chapter selection menu 
02797                 firstChapter = 0
02798                 thumbNode = infoDOM.getElementsByTagName("thumblist")
02799                 if thumbNode.length > 0:
02800                     thumblist = getText(thumbNode[0])
02801                     chapterlist = string.split(thumblist, ",")
02802                     if chapterlist[0] != '00:00:00':
02803                         firstChapter = 1
02804                 x=1
02805                 while x<=chapters:
02806                     #Add this recording to this page's menu...
02807                     button=dvddom.createElement("button")
02808                     button.setAttribute("name","%s" % x)
02809                     if wantDetailsPage: 
02810                         button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, firstChapter + x + 1)))
02811                     else:
02812                         button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, firstChapter + x)))
02813 
02814                     mymenupgc.appendChild(button)
02815                     del button
02816                     x+=1
02817 
02818                 #add the titlemenu button if required
02819                 submenunode = themeDOM.getElementsByTagName("submenu")
02820                 submenunode = submenunode[0]
02821                 titlemenunodes = submenunode.getElementsByTagName("titlemenu")
02822                 if titlemenunodes.length > 0:
02823                     button = dvddom.createElement("button")
02824                     button.setAttribute("name","titlemenu")
02825                     button.appendChild(dvddom.createTextNode("{jump vmgm menu;}"))
02826                     mymenupgc.appendChild(button)
02827                     del button
02828 
02829             titles = dvddom.createElement("titles")
02830             titleset.appendChild(titles)
02831 
02832             # set the right aspect ratio
02833             title_video = dvddom.createElement("video")
02834             title_video.setAttribute("format",videomode)
02835 
02836             if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
02837                 title_video.setAttribute("aspect", "16:9")
02838                 title_video.setAttribute("widescreen", "nopanscan")
02839             else:
02840                 title_video.setAttribute("aspect", "4:3")
02841 
02842             titles.appendChild(title_video)
02843 
02844             #set right audio format
02845             if doesFileExist(os.path.join(getItemTempPath(itemnum), "stream0.mp2")):
02846                 title_audio = dvddom.createElement("audio")
02847                 title_audio.setAttribute("format", "mp2")
02848             else:
02849                 title_audio = dvddom.createElement("audio")
02850                 title_audio.setAttribute("format", "ac3")
02851 
02852             titles.appendChild(title_audio)
02853 
02854             pgc = dvddom.createElement("pgc")
02855             titles.appendChild(pgc)
02856             #pgc.setAttribute("pause","inf")
02857 
02858             if wantDetailsPage:
02859                 #add the detail page intro for this item
02860                 vob = dvddom.createElement("vob")
02861                 vob.setAttribute("file",os.path.join(getTempPath(),"details-%s.mpg" % itemnum))
02862                 pgc.appendChild(vob)
02863 
02864             vob = dvddom.createElement("vob")
02865             if wantChapterMenu:
02866                 vob.setAttribute("chapters",
02867                     createVideoChapters(itemnum,
02868                                         chapters,
02869                                         getLengthOfVideo(itemnum),
02870                                         False))
02871             else:
02872                 vob.setAttribute("chapters", 
02873                     createVideoChaptersFixedLength(itemnum,
02874                                                    chapterLength, 
02875                                                    getLengthOfVideo(itemnum)))
02876 
02877             vob.setAttribute("file",os.path.join(getItemTempPath(itemnum),"final.vob"))
02878             pgc.appendChild(vob)
02879 
02880             post = dvddom.createElement("post")
02881             post.appendChild(dvddom.createTextNode("if (g5 eq %s) call vmgm menu %s; call vmgm menu %s;" % (itemnum + 1, autoplaymenu, page + 1)))
02882             pgc.appendChild(post)
02883 
02884             #Quick variable tidy up (not really required under Python)
02885             del titleset
02886             del titles
02887             del menus
02888             del video
02889             del pgc
02890             del vob
02891             del post
02892 
02893             #Loop through all the nodes inside this menu item and pick previous / next buttons
02894             for node in menuitem.childNodes:
02895 
02896                 if node.nodeName=="previous":
02897                     if page>1:
02898                         button=dvddom.createElement("button")
02899                         button.setAttribute("name","previous")
02900                         button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % page ))
02901                         endbuttons.append(button)
02902 
02903 
02904                 elif node.nodeName=="next":
02905                     if itemnum < numberofitems:
02906                         button=dvddom.createElement("button")
02907                         button.setAttribute("name","next")
02908                         button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % (page + 2)))
02909                         endbuttons.append(button)
02910 
02911                 elif node.nodeName=="playall":
02912                    button=dvddom.createElement("button")
02913                    button.setAttribute("name","playall")
02914                    button.appendChild(dvddom.createTextNode("{g5=1; jump menu %s;}" % autoplaymenu))
02915                    endbuttons.append(button)
02916 
02917             #On to the next item
02918             itemnum+=1
02919 
02920         #Move on to the next page
02921         page+=1
02922 
02923         for button in endbuttons:
02924             menupgc.appendChild(button)
02925             del button
02926 
02927     menupgc = dvddom.createElement("pgc")
02928     menus_element.appendChild(menupgc)
02929     menupgc.setAttribute("pause","inf")
02930     menupgc.appendChild( dvddom.createComment("Autoplay hack") )
02931 
02932     dvdcode = ""
02933     while (itemnum > 1):
02934         itemnum-=1
02935         dvdcode += "if (g5 eq %s) {g5 = %s; jump title %s;} " % (itemnum, itemnum + 1, itemnum)
02936     dvdcode += "g5 = 0; jump menu 1;"
02937 
02938     pre = dvddom.createElement("pre")
02939     pre.appendChild(dvddom.createTextNode(dvdcode))
02940     menupgc.appendChild(pre)    
02941 
02942     if wantIntro:
02943         #Menu creation is finished so we know how many pages were created
02944         #add to to jump to the correct one automatically
02945         dvdcode="if (g3 eq 1) {"
02946         while (page>1):
02947             page-=1;
02948             dvdcode+="if (g4 eq %s) " % page
02949             dvdcode+="jump menu %s;" % (page + 1)
02950             if (page>1):
02951                 dvdcode+=" else "
02952         dvdcode+="}"       
02953         vmgm_pre_node.appendChild(dvddom.createTextNode(dvdcode))
02954 
02955     #write(dvddom.toprettyxml())
02956     #Save xml to file
02957     WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
02958 
02959     #Destroy the DOM and free memory
02960     dvddom.unlink()   
02961 
02962 #############################################################
02963 # Creates the DVDAuthor xml file used to create a DVD with no main menu
02964 
02965 def createDVDAuthorXMLNoMainMenu(screensize, numberofitems):
02966     """Creates the xml file for dvdauthor to use the MythBurn menus."""
02967 
02968     # creates a simple DVD with only a chapter menus shown before each video
02969     # can contain an intro movie and each title can have a details page
02970     # displayed before each title
02971 
02972     write( "Creating DVD XML file for dvd author (No Main Menu)")
02973     #FIXME:
02974     assert False
02975 
02976 #############################################################
02977 # Creates the DVDAuthor xml file used to create an Autoplay DVD
02978 
02979 def createDVDAuthorXMLNoMenus(screensize, numberofitems):
02980     """Creates the xml file for dvdauthor containing no menus."""
02981 
02982     # creates a simple DVD with no menus that chains the videos one after the other
02983     # can contain an intro movie and each title can have a details page
02984     # displayed before each title
02985 
02986     write( "Creating DVD XML file for dvd author (No Menus)")
02987 
02988     dvddom = xml.dom.minidom.parseString(
02989                 '''
02990                 <dvdauthor>
02991                     <vmgm>
02992                         <menus lang="en">
02993                             <pgc entry="title" pause="0">
02994                             </pgc>
02995                         </menus>
02996                     </vmgm>
02997                 </dvdauthor>''')
02998 
02999     dvdauthor_element = dvddom.documentElement
03000     menus = dvdauthor_element.childNodes[1].childNodes[1]
03001     menu_pgc = menus.childNodes[1]
03002 
03003     dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
03004     dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
03005 
03006     # create pgc for menu 1 holds the intro if required, blank mpg if not
03007     if wantIntro:
03008         video = dvddom.createElement("video")
03009         video.setAttribute("format", videomode)
03010 
03011         # set aspect ratio
03012         if mainmenuAspectRatio == "4:3":
03013             video.setAttribute("aspect", "4:3")
03014         else:
03015             video.setAttribute("aspect", "16:9")
03016             video.setAttribute("widescreen", "nopanscan")
03017         menus.appendChild(video)
03018 
03019         pre = dvddom.createElement("pre")
03020         pre.appendChild(dvddom.createTextNode("if (g2==1) jump menu 2;"))
03021         menu_pgc.appendChild(pre)
03022 
03023         node = themeDOM.getElementsByTagName("intro")[0]
03024         introFile = node.attributes["filename"].value
03025 
03026         vob = dvddom.createElement("vob")
03027         vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + introFile))
03028         menu_pgc.appendChild(vob)
03029 
03030         post = dvddom.createElement("post")
03031         post.appendChild(dvddom.createTextNode("g2=1; jump menu 2;"))
03032         menu_pgc.appendChild(post)
03033         del menu_pgc
03034         del post
03035         del pre
03036         del vob
03037     else:
03038         pre = dvddom.createElement("pre")
03039         pre.appendChild(dvddom.createTextNode("g2=1;jump menu 2;"))
03040         menu_pgc.appendChild(pre)
03041 
03042         vob = dvddom.createElement("vob")
03043         vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
03044         menu_pgc.appendChild(vob)
03045 
03046         del menu_pgc
03047         del pre
03048         del vob
03049 
03050     # create menu 2 - dummy menu that allows us to jump to each titleset in sequence
03051     menu_pgc = dvddom.createElement("pgc")
03052     menu_pgc.setAttribute("pause", "0")
03053 
03054     preText = "if (g1==0) g1=1;"
03055     for i in range(numberofitems):
03056         preText += "if (g1==%d) jump titleset %d menu;" % (i + 1, i + 1)
03057 
03058     pre = dvddom.createElement("pre")
03059     pre.appendChild(dvddom.createTextNode(preText))
03060     menu_pgc.appendChild(pre)
03061 
03062     vob = dvddom.createElement("vob")
03063     vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
03064     menu_pgc.appendChild(vob)
03065     menus.appendChild(menu_pgc)
03066 
03067     # for each title add a <titleset> section
03068     itemNum = 1
03069     while itemNum <= numberofitems:
03070         write( "Adding item %s" % itemNum)
03071 
03072         titleset = dvddom.createElement("titleset")
03073         dvdauthor_element.appendChild(titleset)
03074 
03075         # create menu
03076         menu = dvddom.createElement("menus")
03077         menupgc = dvddom.createElement("pgc")
03078         menu.appendChild(menupgc)
03079         menupgc.setAttribute("pause","0")
03080         titleset.appendChild(menu)
03081 
03082         if wantDetailsPage:
03083             #add the detail page intro for this item
03084             vob = dvddom.createElement("vob")
03085             vob.setAttribute("file", os.path.join(getTempPath(),"details-%s.mpg" % itemNum))
03086             menupgc.appendChild(vob)
03087 
03088             post = dvddom.createElement("post")
03089             post.appendChild(dvddom.createTextNode("jump title 1;"))
03090             menupgc.appendChild(post)
03091             del post
03092         else:
03093             #add dummy menu for this item
03094             pre = dvddom.createElement("pre")
03095             pre.appendChild(dvddom.createTextNode("jump title 1;"))
03096             menupgc.appendChild(pre)
03097             del pre
03098 
03099             vob = dvddom.createElement("vob")
03100             vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
03101             menupgc.appendChild(vob)
03102 
03103         titles = dvddom.createElement("titles")
03104 
03105         # set the right aspect ratio
03106         title_video = dvddom.createElement("video")
03107         title_video.setAttribute("format", videomode)
03108 
03109         # use aspect ratio of video
03110         if getAspectRatioOfVideo(itemNum) > aspectRatioThreshold:
03111             title_video.setAttribute("aspect", "16:9")
03112             title_video.setAttribute("widescreen", "nopanscan")
03113         else:
03114             title_video.setAttribute("aspect", "4:3")
03115 
03116         titles.appendChild(title_video)
03117 
03118         pgc = dvddom.createElement("pgc")
03119 
03120         vob = dvddom.createElement("vob")
03121         vob.setAttribute("file", os.path.join(getItemTempPath(itemNum), "final.vob"))
03122         vob.setAttribute("chapters", createVideoChaptersFixedLength(itemNum,
03123                                                                     chapterLength,
03124                                                                     getLengthOfVideo(itemNum)))
03125         pgc.appendChild(vob)
03126 
03127         del vob
03128         del menupgc
03129 
03130         post = dvddom.createElement("post")
03131         if itemNum == numberofitems:
03132             post.appendChild(dvddom.createTextNode("exit;"))
03133         else:
03134             post.appendChild(dvddom.createTextNode("g1=%d;call vmgm menu 2;" % (itemNum + 1)))
03135 
03136         pgc.appendChild(post)
03137 
03138         titles.appendChild(pgc)
03139         titleset.appendChild(titles)
03140 
03141         del pgc
03142         del titles
03143         del title_video
03144         del post
03145         del titleset
03146 
03147         itemNum +=1
03148 
03149     #Save xml to file
03150     WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
03151 
03152     #Destroy the DOM and free memory
03153     dvddom.unlink()
03154 
03155 #############################################################
03156 # Creates the directory to hold the preview images for an animated menu 
03157 
03158 def createEmptyPreviewFolder(videoitem):
03159     previewfolder = os.path.join(getItemTempPath(videoitem), "preview")
03160     if os.path.exists(previewfolder):
03161         deleteAllFilesInFolder(previewfolder)
03162         os.rmdir (previewfolder)
03163     os.makedirs(previewfolder)
03164     return previewfolder
03165 
03166 #############################################################
03167 # Generates the thumbnail images used to create animated menus
03168 
03169 def generateVideoPreview(videoitem, itemonthispage, menuitem, starttime, menulength, previewfolder):
03170     """generate thumbnails for a preview in a menu"""
03171 
03172     positionx = 9999
03173     positiony = 9999
03174     width = 0
03175     height = 0
03176     maskpicture = None
03177 
03178     #run through the theme items and find any graphics that is using a movie identifier
03179     for node in menuitem.childNodes:
03180         if node.nodeName=="graphic":
03181             if node.attributes["filename"].value == "%movie":
03182                 #This is a movie preview item so we need to generate the thumbnails
03183                 inputfile = os.path.join(getItemTempPath(videoitem),"stream.mv2")
03184                 outputfile = os.path.join(previewfolder, "preview-i%d-t%%1-f%%2.jpg" % itemonthispage)
03185                 width = getScaledAttribute(node, "w")
03186                 height = getScaledAttribute(node, "h")
03187                 frames = int(secondsToFrames(menulength))
03188 
03189                 command = "mytharchivehelper -q -q --createthumbnail --infile  %s --thumblist '%s' --outfile %s --framecount %d" % (quoteCmdArg(inputfile), starttime, quoteCmdArg(outputfile), frames)
03190                 result = runCommand(command)
03191                 if (result != 0):
03192                     write( "mytharchivehelper failed with code %d. Command = %s" % (result, command) )
03193 
03194                 positionx = getScaledAttribute(node, "x")
03195                 positiony = getScaledAttribute(node, "y")
03196 
03197                 #see if this graphics item has a mask
03198                 if node.hasAttribute("mask"):
03199                     imagemaskfilename = getThemeFile(themeName, node.attributes["mask"].value)
03200                     if node.attributes["mask"].value <> "" and doesFileExist(imagemaskfilename):
03201                         maskpicture = Image.open(imagemaskfilename,"r").resize((width, height))
03202                         maskpicture = maskpicture.convert("RGBA")
03203 
03204     return (positionx, positiony, width, height, maskpicture)
03205 
03206 #############################################################
03207 # Draws text and graphics onto a dvd menu
03208 
03209 def drawThemeItem(page, itemsonthispage, itemnum, menuitem, bgimage, draw,
03210                   bgimagemask, drawmask, highlightcolor, spumuxdom, spunode,
03211                   numberofitems, chapternumber, chapterlist):
03212     """Draws text and graphics onto a dvd menu, called by 
03213        createMenu and createChapterMenu"""
03214 
03215     #Get the XML containing information about this item
03216     infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum), "info.xml"))
03217 
03218     #Error out if its the wrong XML
03219     if infoDOM.documentElement.tagName != "fileinfo":
03220         fatalError("The info.xml file (%s) doesn't look right" %
03221                     os.path.join(getItemTempPath(itemnum),"info.xml"))
03222 
03223     #boundarybox holds the max and min dimensions for this item 
03224     #so we can auto build a menu highlight box
03225     boundarybox = 9999,9999,0,0
03226     wantHighlightBox = True
03227 
03228     #Loop through all the nodes inside this menu item
03229     for node in menuitem.childNodes:
03230 
03231         #Process each type of item to add it onto the background image
03232         if node.nodeName=="graphic":
03233             #Overlay graphic image onto background
03234 
03235             # draw background if required
03236             paintBackground(bgimage, node)
03237 
03238             # if this graphic item is a movie thumbnail then we dont process it here
03239             if node.attributes["filename"].value == "%movie":
03240                 # this is a movie item but we must still update the boundary box
03241                 boundarybox = checkBoundaryBox(boundarybox, node)
03242             else:
03243                 imagefilename = expandItemText(infoDOM,
03244                                                node.attributes["filename"].value,
03245                                                itemnum, page, itemsonthispage,
03246                                                chapternumber, chapterlist)
03247 
03248                 if doesFileExist(imagefilename) == False:
03249                     if imagefilename == node.attributes["filename"].value:
03250                         imagefilename = getThemeFile(themeName,
03251                                         node.attributes["filename"].value)
03252 
03253                 # see if an image mask exists
03254                 maskfilename = None
03255                 if node.hasAttribute("mask") and node.attributes["mask"].value <> "":
03256                     maskfilename = getThemeFile(themeName, node.attributes["mask"].value)
03257 
03258                 # if this is a thumb image and is a MythVideo coverart image then preserve 
03259                 # its aspect ratio unless overriden later by the theme
03260                 if (node.attributes["filename"].value == "%thumbnail"
03261                   and getText(infoDOM.getElementsByTagName("coverfile")[0]) !=""):
03262                     stretch = False
03263                 else:
03264                     stretch = True
03265 
03266                 if paintImage(imagefilename, maskfilename, node, bgimage, stretch):
03267                     boundarybox = checkBoundaryBox(boundarybox, node)
03268                 else:
03269                     write("Image file does not exist '%s'" % imagefilename)
03270 
03271         elif node.nodeName == "text":
03272             # Apply some text to the background, including wordwrap if required.
03273 
03274             # draw background if required
03275             paintBackground(bgimage, node)
03276 
03277             text = expandItemText(infoDOM,node.attributes["value"].value,
03278                                   itemnum, page, itemsonthispage,
03279                                   chapternumber, chapterlist)
03280 
03281             if text>"":
03282                 paintText(draw, bgimage, text, node)
03283 
03284             boundarybox = checkBoundaryBox(boundarybox, node)
03285             del text
03286 
03287         elif node.nodeName=="previous":
03288             if page>1:
03289                 #Overlay previous graphic button onto background
03290 
03291                 # draw background if required
03292                 paintBackground(bgimage, node)
03293 
03294                 paintButton(draw, bgimage, bgimagemask, node, infoDOM,
03295                             itemnum, page, itemsonthispage, chapternumber,
03296                             chapterlist)
03297 
03298                 button = spumuxdom.createElement("button")
03299                 button.setAttribute("name","previous")
03300                 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
03301                 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
03302                 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") + 
03303                                                 getScaledAttribute(node, "w")))
03304                 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
03305                                                 getScaledAttribute(node, "h")))
03306                 spunode.appendChild(button)
03307 
03308                 write( "Added previous page button")
03309 
03310 
03311         elif node.nodeName == "next":
03312             if itemnum < numberofitems:
03313                 #Overlay next graphic button onto background
03314 
03315                 # draw background if required
03316                 paintBackground(bgimage, node)
03317 
03318                 paintButton(draw, bgimage, bgimagemask, node, infoDOM,
03319                             itemnum, page, itemsonthispage, chapternumber,
03320                             chapterlist)
03321 
03322                 button = spumuxdom.createElement("button")
03323                 button.setAttribute("name","next")
03324                 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
03325                 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
03326                 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") + 
03327                                                  getScaledAttribute(node, "w")))
03328                 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") + 
03329                                                  getScaledAttribute(node, "h")))
03330                 spunode.appendChild(button)
03331 
03332                 write("Added next page button")
03333 
03334         elif node.nodeName=="playall":
03335             #Overlay playall graphic button onto background
03336 
03337             # draw background if required
03338             paintBackground(bgimage, node)
03339 
03340             paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
03341                         itemsonthispage, chapternumber, chapterlist)
03342 
03343             button = spumuxdom.createElement("button")
03344             button.setAttribute("name","playall")
03345             button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
03346             button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
03347             button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") + 
03348                                              getScaledAttribute(node, "w")))
03349             button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
03350                                              getScaledAttribute(node, "h")))
03351             spunode.appendChild(button)
03352 
03353             write("Added playall button")
03354 
03355         elif node.nodeName == "titlemenu":
03356             if itemnum < numberofitems:
03357                 #Overlay next graphic button onto background
03358 
03359                 # draw background if required
03360                 paintBackground(bgimage, node)
03361 
03362                 paintButton(draw, bgimage, bgimagemask, node, infoDOM, 
03363                             itemnum, page, itemsonthispage, chapternumber, 
03364                             chapterlist)
03365 
03366                 button = spumuxdom.createElement("button")
03367                 button.setAttribute("name","titlemenu")
03368                 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
03369                 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
03370                 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") +
03371                                                 getScaledAttribute(node, "w")))
03372                 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
03373                                                 getScaledAttribute(node, "h")))
03374                 spunode.appendChild(button)
03375 
03376                 write( "Added titlemenu button")
03377 
03378         elif node.nodeName=="button":
03379             #Overlay item graphic/text button onto background
03380 
03381             # draw background if required
03382             paintBackground(bgimage, node)
03383 
03384             wantHighlightBox = False
03385 
03386             paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
03387                         itemsonthispage, chapternumber, chapterlist)
03388 
03389             boundarybox = checkBoundaryBox(boundarybox, node)
03390 
03391 
03392         elif node.nodeName=="#text" or node.nodeName=="#comment":
03393             #Do nothing
03394             assert True
03395         else:
03396             write( "Dont know how to process %s" % node.nodeName)
03397 
03398     if drawmask == None:
03399         return
03400 
03401     #Draw the selection mask for this item
03402     if wantHighlightBox == True:
03403         # Make the boundary box bigger than the content to avoid over writing it
03404         boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
03405         drawmask.rectangle(boundarybox,outline=highlightcolor)
03406 
03407         # Draw another line to make the box thicker - PIL does not support linewidth
03408         boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
03409         drawmask.rectangle(boundarybox,outline=highlightcolor)
03410 
03411     node = spumuxdom.createElement("button")
03412     #Fiddle this for chapter marks....
03413     if chapternumber>0:
03414         node.setAttribute("name","%s" % chapternumber)
03415     else:
03416         node.setAttribute("name","%s" % itemnum)
03417     node.setAttribute("x0","%d" % int(boundarybox[0]))
03418     node.setAttribute("y0","%d" % int(boundarybox[1]))
03419     node.setAttribute("x1","%d" % int(boundarybox[2] + 1))
03420     node.setAttribute("y1","%d" % int(boundarybox[3] + 1))
03421     spunode.appendChild(node)
03422 
03423 #############################################################
03424 # creates the main menu for a DVD
03425 
03426 def createMenu(screensize, screendpi, numberofitems):
03427     """Creates all the necessary menu images and files for the MythBurn menus."""
03428 
03429     #Get the main menu node (we must only have 1)
03430     menunode=themeDOM.getElementsByTagName("menu")
03431     if menunode.length!=1:
03432         fatalError("Cannot find menu element in theme file")
03433     menunode=menunode[0]
03434 
03435     menuitems=menunode.getElementsByTagName("item")
03436     #Total number of video items on a single menu page (no less than 1!)
03437     itemsperpage = menuitems.length
03438     write( "Menu items per page %s" % itemsperpage)
03439 
03440     #Get background image filename
03441     backgroundfilename = menunode.attributes["background"].value
03442     if backgroundfilename=="":
03443         fatalError("Background image is not set in theme file")
03444 
03445     backgroundfilename = getThemeFile(themeName,backgroundfilename)
03446     write( "Background image file is %s" % backgroundfilename)
03447     if not doesFileExist(backgroundfilename):
03448         fatalError("Background image not found (%s)" % backgroundfilename)
03449 
03450     #Get highlight color
03451     highlightcolor = "red"
03452     if menunode.hasAttribute("highlightcolor"):
03453         highlightcolor = menunode.attributes["highlightcolor"].value
03454 
03455     #Get menu music
03456     menumusic = "menumusic.ac3"
03457     if menunode.hasAttribute("music"):
03458         menumusic = menunode.attributes["music"].value
03459 
03460     #Get menu length
03461     menulength = 15
03462     if menunode.hasAttribute("length"):
03463         menulength = int(menunode.attributes["length"].value)
03464 
03465     write("Music is %s, length is %s seconds" % (menumusic, menulength))
03466 
03467     #Page number counter
03468     page=1
03469 
03470     #Item counter to indicate current video item
03471     itemnum=1
03472 
03473     write("Creating DVD menus")
03474 
03475     while itemnum <= numberofitems:
03476         write("Menu page %s" % page)
03477 
03478         #need to check if any of the videos are flaged as movies
03479         #and if so generate the required preview
03480 
03481         write("Creating Preview Video")
03482         previewitem = itemnum
03483         itemsonthispage = 0
03484         haspreview = False
03485 
03486         previewx = []
03487         previewy = []
03488         previeww = []
03489         previewh = []
03490         previewmask = []
03491 
03492         while previewitem <= numberofitems and itemsonthispage < itemsperpage:
03493             menuitem=menuitems[ itemsonthispage ]
03494             itemsonthispage+=1
03495 
03496             #make sure the preview folder is empty and present
03497             previewfolder = createEmptyPreviewFolder(previewitem)
03498 
03499             #and then generate the preview if required (px=9999 means not required)
03500             px, py, pw, ph, maskimage = generateVideoPreview(previewitem, itemsonthispage, menuitem, 0, menulength, previewfolder)
03501             previewx.append(px)
03502             previewy.append(py)
03503             previeww.append(pw)
03504             previewh.append(ph)
03505             previewmask.append(maskimage)
03506             if px != 9999:
03507                 haspreview = True
03508 
03509             previewitem+=1
03510 
03511         #previews generated but need to save where we started from
03512         savedpreviewitem = itemnum
03513 
03514         #Number of video items on this menu page
03515         itemsonthispage=0
03516 
03517         #instead of loading the background image and drawing on it we now
03518         #make a transparent image and draw all items on it. This overlay
03519         #image is then added to the required background image when the
03520         #preview items are added (the reason for this is it will assist
03521         #if the background image is actually a video)
03522 
03523         overlayimage=Image.new("RGBA",screensize)
03524         draw=ImageDraw.Draw(overlayimage)
03525 
03526         #Create image to hold button masks (same size as background)
03527         bgimagemask=Image.new("RGBA",overlayimage.size)
03528         drawmask=ImageDraw.Draw(bgimagemask)
03529 
03530         spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
03531         spunode = spumuxdom.documentElement.firstChild.firstChild
03532 
03533         #Loop through all the items on this menu page
03534         while itemnum <= numberofitems and itemsonthispage < itemsperpage:
03535             menuitem=menuitems[ itemsonthispage ]
03536 
03537             itemsonthispage+=1
03538 
03539             drawThemeItem(page, itemsonthispage,
03540                         itemnum, menuitem, overlayimage,
03541                         draw, bgimagemask, drawmask, highlightcolor,
03542                         spumuxdom, spunode, numberofitems, 0,"")
03543 
03544             #On to the next item
03545             itemnum+=1
03546 
03547         #Paste the overlay image onto the background
03548         bgimage=Image.open(backgroundfilename,"r").resize(screensize)
03549         bgimage.paste(overlayimage, (0,0), overlayimage)
03550 
03551         #Save this menu image and its mask
03552         bgimage.save(os.path.join(getTempPath(),"background-%s.jpg" % page),"JPEG", quality=99)
03553         bgimagemask.save(os.path.join(getTempPath(),"backgroundmask-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
03554 
03555         #now that the base background has been made and all the previews generated
03556         #we need to add the previews to the background
03557         #Assumption: We assume that there is nothing in the location of where the items go 
03558         #(ie, no text on the images)
03559 
03560         itemsonthispage = 0
03561 
03562         #numframes should be the number of preview images that have been created
03563         numframes=secondsToFrames(menulength)
03564 
03565         # only generate the preview video if required.
03566         if haspreview == True:
03567             write( "Generating the preview images" )
03568             framenum = 0
03569             while framenum < numframes:
03570                 previewitem = savedpreviewitem
03571                 itemsonthispage = 0
03572                 while previewitem <= numberofitems and itemsonthispage < itemsperpage:
03573                     itemsonthispage+=1
03574                     if previewx[itemsonthispage-1] != 9999:
03575                         previewpath = os.path.join(getItemTempPath(previewitem), "preview")
03576                         previewfile = "preview-i%d-t1-f%d.jpg" % (itemsonthispage, framenum)
03577                         imagefile = os.path.join(previewpath, previewfile)
03578 
03579                         if doesFileExist(imagefile):
03580                             picture = Image.open(imagefile, "r").resize((previeww[itemsonthispage-1], previewh[itemsonthispage-1]))
03581                             picture = picture.convert("RGBA")
03582                             imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % itemsonthispage)
03583                             if previewmask[itemsonthispage-1] != None:
03584                                 bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1]), previewmask[itemsonthispage-1])
03585                             else:
03586                                 bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1]))
03587                             del picture
03588                     previewitem+=1
03589                 #bgimage.save(os.path.join(getTempPath(),"background-%s-f%06d.png" % (page, framenum)),"PNG",quality=100,optimize=0,dpi=screendpi)
03590                 bgimage.save(os.path.join(getTempPath(),"background-%s-f%06d.jpg" % (page, framenum)),"JPEG",quality=99)
03591                 framenum+=1
03592 
03593         spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
03594         spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
03595 
03596         #Release large amounts of memory ASAP !
03597         del draw
03598         del bgimage
03599         del drawmask
03600         del bgimagemask
03601         del overlayimage
03602         del previewx
03603         del previewy
03604         del previewmask
03605 
03606         WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"spumux-%s.xml" % page))
03607 
03608         if mainmenuAspectRatio == "4:3":
03609             aspect_ratio = 2
03610         else:
03611             aspect_ratio = 3
03612 
03613         write("Encoding Menu Page %s using aspect ratio '%s'" % (page, mainmenuAspectRatio))
03614         if haspreview == True:
03615             encodeMenu(os.path.join(getTempPath(),"background-%s-f%%06d.jpg" % page),
03616                         os.path.join(getTempPath(),"temp.m2v"),
03617                         getThemeFile(themeName,menumusic),
03618                         menulength,
03619                         os.path.join(getTempPath(),"temp.mpg"),
03620                         os.path.join(getTempPath(),"spumux-%s.xml" % page),
03621                         os.path.join(getTempPath(),"menu-%s.mpg" % page),
03622                         aspect_ratio)
03623         else:
03624             encodeMenu(os.path.join(getTempPath(),"background-%s.jpg" % page),
03625                         os.path.join(getTempPath(),"temp.m2v"),
03626                         getThemeFile(themeName,menumusic),
03627                         menulength,
03628                         os.path.join(getTempPath(),"temp.mpg"),
03629                         os.path.join(getTempPath(),"spumux-%s.xml" % page),
03630                         os.path.join(getTempPath(),"menu-%s.mpg" % page),
03631                         aspect_ratio)
03632 
03633         #Move on to the next page
03634         page+=1
03635 
03636 #############################################################
03637 # creates a chapter menu for a file on a DVD
03638 
03639 def createChapterMenu(screensize, screendpi, numberofitems):
03640     """Creates all the necessary menu images and files for the MythBurn menus."""
03641 
03642     #Get the main menu node (we must only have 1)
03643     menunode=themeDOM.getElementsByTagName("submenu")
03644     if menunode.length!=1:
03645         fatalError("Cannot find submenu element in theme file")
03646     menunode=menunode[0]
03647 
03648     menuitems=menunode.getElementsByTagName("chapter")
03649     #Total number of video items on a single menu page (no less than 1!)
03650     itemsperpage = menuitems.length
03651     write( "Chapter items per page %s " % itemsperpage)
03652 
03653     #Get background image filename
03654     backgroundfilename = menunode.attributes["background"].value
03655     if backgroundfilename=="":
03656         fatalError("Background image is not set in theme file")
03657     backgroundfilename = getThemeFile(themeName,backgroundfilename)
03658     write( "Background image file is %s" % backgroundfilename)
03659     if not doesFileExist(backgroundfilename):
03660         fatalError("Background image not found (%s)" % backgroundfilename)
03661 
03662     #Get highlight color
03663     highlightcolor = "red"
03664     if menunode.hasAttribute("highlightcolor"):
03665         highlightcolor = menunode.attributes["highlightcolor"].value
03666 
03667     #Get menu music
03668     menumusic = "menumusic.ac3"
03669     if menunode.hasAttribute("music"):
03670         menumusic = menunode.attributes["music"].value
03671 
03672     #Get menu length
03673     menulength = 15
03674     if menunode.hasAttribute("length"):
03675         menulength = int(menunode.attributes["length"].value)
03676 
03677     write("Music is %s, length is %s seconds" % (menumusic, menulength))
03678 
03679     #Page number counter
03680     page=1
03681 
03682     write( "Creating DVD sub-menus")
03683 
03684     while page <= numberofitems:
03685         write( "Sub-menu %s " % page)
03686 
03687         #instead of loading the background image and drawing on it we now
03688         #make a transparent image and draw all items on it. This overlay
03689         #image is then added to the required background image when the
03690         #preview items are added (the reason for this is it will assist
03691         #if the background image is actually a video)
03692 
03693         overlayimage=Image.new("RGBA",screensize, (0,0,0,0))
03694         draw=ImageDraw.Draw(overlayimage)
03695 
03696         #Create image to hold button masks (same size as background)
03697         bgimagemask=Image.new("RGBA",overlayimage.size, (0,0,0,0))
03698         drawmask=ImageDraw.Draw(bgimagemask)
03699 
03700         spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
03701         spunode = spumuxdom.documentElement.firstChild.firstChild
03702 
03703         #Extract the thumbnails
03704         chapterlist=createVideoChapters(page,itemsperpage,getLengthOfVideo(page),True)
03705         chapterlist=string.split(chapterlist,",")
03706 
03707         #now need to preprocess the menu to see if any preview videos are required
03708         #This must be done on an individual basis since we do the resize as the
03709         #images are extracted.
03710 
03711         #first make sure the preview folder is empty and present
03712         previewfolder = createEmptyPreviewFolder(page)
03713 
03714         haspreview = False
03715 
03716         previewsegment=int(getLengthOfVideo(page) / itemsperpage)
03717         previewtime = 0
03718         previewchapter = 0
03719         previewx = []
03720         previewy = []
03721         previeww = []
03722         previewh = []
03723         previewmask = []
03724 
03725         while previewchapter < itemsperpage:
03726             menuitem=menuitems[ previewchapter ]
03727 
03728             #generate the preview if required (px=9999 means not required)
03729             px, py, pw, ph, maskimage = generateVideoPreview(page, previewchapter, menuitem, previewtime, menulength, previewfolder)
03730             previewx.append(px)
03731             previewy.append(py)
03732             previeww.append(pw)
03733             previewh.append(ph)
03734             previewmask.append(maskimage)
03735 
03736             if px != 9999:
03737                 haspreview = True
03738 
03739             previewchapter+=1
03740             previewtime+=previewsegment
03741 
03742         #Loop through all the items on this menu page
03743         chapter=0
03744         while chapter < itemsperpage:  # and itemsonthispage < itemsperpage:
03745             menuitem=menuitems[ chapter ]
03746             chapter+=1
03747 
03748             drawThemeItem(page, itemsperpage, page, menuitem,
03749                         overlayimage, draw, 
03750                         bgimagemask, drawmask, highlightcolor,
03751                         spumuxdom, spunode,
03752                         999, chapter, chapterlist)
03753 
03754         #Save this menu image and its mask
03755         bgimage=Image.open(backgroundfilename,"r").resize(screensize)
03756         bgimage.paste(overlayimage, (0,0), overlayimage)
03757         bgimage.save(os.path.join(getTempPath(),"chaptermenu-%s.jpg" % page),"JPEG", quality=99)
03758 
03759         bgimagemask.save(os.path.join(getTempPath(),"chaptermenumask-%s.png" % page),"PNG",quality=90,optimize=0)
03760 
03761         if haspreview == True:
03762             numframes=secondsToFrames(menulength)
03763 
03764             #numframes should be the number of preview images that have been created
03765 
03766             write( "Generating the preview images" )
03767             framenum = 0
03768             while framenum < numframes:
03769                 previewchapter = 0
03770                 while previewchapter < itemsperpage:
03771                     if previewx[previewchapter] != 9999:
03772                         previewpath = os.path.join(getItemTempPath(page), "preview")
03773                         previewfile = "preview-i%d-t1-f%d.jpg" % (previewchapter, framenum)
03774                         imagefile = os.path.join(previewpath, previewfile)
03775 
03776                         if doesFileExist(imagefile):
03777                             picture = Image.open(imagefile, "r").resize((previeww[previewchapter], previewh[previewchapter]))
03778                             picture = picture.convert("RGBA")
03779                             imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % previewchapter)
03780                             if previewmask[previewchapter] != None:
03781                                 bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter]), previewmask[previewchapter])
03782                             else:
03783                                 bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter]))
03784                             del picture
03785                     previewchapter+=1
03786                 bgimage.save(os.path.join(getTempPath(),"chaptermenu-%s-f%06d.jpg" % (page, framenum)),"JPEG",quality=99)
03787                 framenum+=1
03788 
03789         spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
03790         spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
03791 
03792         #Release large amounts of memory ASAP !
03793         del draw
03794         del bgimage
03795         del drawmask
03796         del bgimagemask
03797         del overlayimage
03798         del previewx
03799         del previewy
03800         del previewmask
03801 
03802         #write( spumuxdom.toprettyxml())
03803         WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"chapterspumux-%s.xml" % page))
03804 
03805         if chaptermenuAspectRatio == "4:3":
03806             aspect_ratio = '2'
03807         elif chaptermenuAspectRatio == "16:9":
03808             aspect_ratio = '3'
03809         else: 
03810             if getAspectRatioOfVideo(page) > aspectRatioThreshold:
03811                 aspect_ratio = '3'
03812             else:
03813                 aspect_ratio = '2'
03814 
03815         write("Encoding Chapter Menu Page %s using aspect ratio '%s'" % (page, chaptermenuAspectRatio))
03816 
03817         if haspreview == True:
03818             encodeMenu(os.path.join(getTempPath(),"chaptermenu-%s-f%%06d.jpg" % page),
03819                         os.path.join(getTempPath(),"temp.m2v"),
03820                         getThemeFile(themeName,menumusic),
03821                         menulength,
03822                         os.path.join(getTempPath(),"temp.mpg"),
03823                         os.path.join(getTempPath(),"chapterspumux-%s.xml" % page),
03824                         os.path.join(getTempPath(),"chaptermenu-%s.mpg" % page),
03825                         aspect_ratio)
03826         else:
03827             encodeMenu(os.path.join(getTempPath(),"chaptermenu-%s.jpg" % page),
03828                         os.path.join(getTempPath(),"temp.m2v"),
03829                         getThemeFile(themeName,menumusic),
03830                         menulength,
03831                         os.path.join(getTempPath(),"temp.mpg"),
03832                         os.path.join(getTempPath(),"chapterspumux-%s.xml" % page),
03833                         os.path.join(getTempPath(),"chaptermenu-%s.mpg" % page),
03834                         aspect_ratio)
03835 
03836         #Move on to the next page
03837         page+=1
03838 
03839 #############################################################
03840 # creates the details page for a file on a DVD
03841 
03842 def createDetailsPage(screensize, screendpi, numberofitems):
03843     """Creates all the necessary images and files for the details page."""
03844 
03845     write( "Creating details pages")
03846 
03847     #Get the detailspage node (we must only have 1)
03848     detailnode=themeDOM.getElementsByTagName("detailspage")
03849     if detailnode.length!=1:
03850         fatalError("Cannot find detailspage element in theme file")
03851     detailnode=detailnode[0]
03852 
03853     #Get background image filename
03854     backgroundfilename = detailnode.attributes["background"].value
03855     if backgroundfilename=="":
03856         fatalError("Background image is not set in theme file")
03857     backgroundfilename = getThemeFile(themeName,backgroundfilename)
03858     write( "Background image file is %s" % backgroundfilename)
03859     if not doesFileExist(backgroundfilename):
03860         fatalError("Background image not found (%s)" % backgroundfilename)
03861 
03862     #Get menu music
03863     menumusic = "menumusic.ac3"
03864     if detailnode.hasAttribute("music"):
03865         menumusic = detailnode.attributes["music"].value
03866 
03867     #Get menu length
03868     menulength = 15
03869     if detailnode.hasAttribute("length"):
03870         menulength = int(detailnode.attributes["length"].value)
03871 
03872     write("Music is %s, length is %s seconds" % (menumusic, menulength))
03873 
03874     #Item counter to indicate current video item
03875     itemnum=1
03876 
03877     while itemnum <= numberofitems:
03878         write( "Creating details page for %s" % itemnum)
03879 
03880         #make sure the preview folder is empty and present
03881         previewfolder = createEmptyPreviewFolder(itemnum)
03882         haspreview = False
03883 
03884         #and then generate the preview if required (px=9999 means not required)
03885         previewx, previewy, previeww, previewh, previewmask = generateVideoPreview(itemnum, 1, detailnode, 0, menulength, previewfolder)
03886         if previewx != 9999:
03887             haspreview = True
03888 
03889         #instead of loading the background image and drawing on it we now
03890         #make a transparent image and draw all items on it. This overlay
03891         #image is then added to the required background image when the
03892         #preview items are added (the reason for this is it will assist
03893         #if the background image is actually a video)
03894 
03895         overlayimage=Image.new("RGBA",screensize, (0,0,0,0))
03896         draw=ImageDraw.Draw(overlayimage)
03897 
03898         spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
03899         spunode = spumuxdom.documentElement.firstChild.firstChild
03900 
03901         drawThemeItem(0, 0, itemnum, detailnode, overlayimage, draw, None, None,
03902                       "", spumuxdom, spunode, numberofitems, 0, "")
03903 
03904         #Save this details image
03905         bgimage=Image.open(backgroundfilename,"r").resize(screensize)
03906         bgimage.paste(overlayimage, (0,0), overlayimage)
03907         bgimage.save(os.path.join(getTempPath(),"details-%s.jpg" % itemnum),"JPEG", quality=99)
03908 
03909         if haspreview == True:
03910             numframes=secondsToFrames(menulength)
03911 
03912             #numframes should be the number of preview images that have been created
03913             write( "Generating the detail preview images" )
03914             framenum = 0
03915             while framenum < numframes:
03916                 if previewx != 9999:
03917                     previewpath = os.path.join(getItemTempPath(itemnum), "preview")
03918                     previewfile = "preview-i%d-t1-f%d.jpg" % (1, framenum)
03919                     imagefile = os.path.join(previewpath, previewfile)
03920 
03921                     if doesFileExist(imagefile):
03922                         picture = Image.open(imagefile, "r").resize((previeww, previewh))
03923                         picture = picture.convert("RGBA")
03924                         imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % 1)
03925                         if previewmask != None:
03926                             bgimage.paste(picture, (previewx, previewy), previewmask)
03927                         else:
03928                             bgimage.paste(picture, (previewx, previewy))
03929                         del picture
03930                 bgimage.save(os.path.join(getTempPath(),"details-%s-f%06d.jpg" % (itemnum, framenum)),"JPEG",quality=99)
03931                 framenum+=1
03932 
03933 
03934         #Release large amounts of memory ASAP !
03935         del draw
03936         del bgimage
03937 
03938         # always use the same aspect ratio as the video
03939         aspect_ratio='2'
03940         if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
03941             aspect_ratio='3'
03942 
03943         #write( spumuxdom.toprettyxml())
03944         WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"detailsspumux-%s.xml" % itemnum))
03945 
03946         write("Encoding Details Page %s" % itemnum)
03947         if haspreview == True:
03948             encodeMenu(os.path.join(getTempPath(),"details-%s-f%%06d.jpg" % itemnum),
03949                         os.path.join(getTempPath(),"temp.m2v"),
03950                         getThemeFile(themeName,menumusic),
03951                         menulength,
03952                         os.path.join(getTempPath(),"temp.mpg"),
03953                         "",
03954                         os.path.join(getTempPath(),"details-%s.mpg" % itemnum),
03955                         aspect_ratio)
03956         else:
03957             encodeMenu(os.path.join(getTempPath(),"details-%s.jpg" % itemnum),
03958                         os.path.join(getTempPath(),"temp.m2v"),
03959                         getThemeFile(themeName,menumusic),
03960                         menulength,
03961                         os.path.join(getTempPath(),"temp.mpg"),
03962                         "",
03963                         os.path.join(getTempPath(),"details-%s.mpg" % itemnum),
03964                         aspect_ratio)
03965 
03966         #On to the next item
03967         itemnum+=1
03968 
03969 #############################################################
03970 # checks if a file is an avi file
03971 
03972 def isMediaAVIFile(file):
03973     fh = open(file, 'rb')
03974     Magic = fh.read(4)
03975     fh.close()
03976     return Magic=="RIFF"
03977 
03978 #############################################################
03979 # checks to see if an audio stream need to be converted to ac3 
03980 
03981 def processAudio(folder):
03982     """encode audio to ac3 for better compression and compatability with NTSC players"""
03983 
03984     # process track 1
03985     if not encodetoac3 and doesFileExist(os.path.join(folder,'stream0.mp2')):
03986         #don't re-encode to ac3 if the user doesn't want it
03987         write( "Audio track 1 is in mp2 format - NOT re-encoding to ac3")
03988     elif doesFileExist(os.path.join(folder,'stream0.mp2'))==True:
03989         write( "Audio track 1 is in mp2 format - re-encoding to ac3")
03990         encodeAudio("ac3",os.path.join(folder,'stream0.mp2'), os.path.join(folder,'stream0.ac3'),True)
03991     elif doesFileExist(os.path.join(folder,'stream0.mpa'))==True:
03992         write( "Audio track 1 is in mpa format - re-encoding to ac3")
03993         encodeAudio("ac3",os.path.join(folder,'stream0.mpa'), os.path.join(folder,'stream0.ac3'),True)
03994     elif doesFileExist(os.path.join(folder,'stream0.ac3'))==True:
03995         write( "Audio is already in ac3 format")
03996     else:
03997         fatalError("Track 1 - Unknown audio format or de-multiplex failed!")
03998 
03999     # process track 2
04000     if not encodetoac3 and doesFileExist(os.path.join(folder,'stream1.mp2')):
04001         #don't re-encode to ac3 if the user doesn't want it
04002         write( "Audio track 2 is in mp2 format - NOT re-encoding to ac3")
04003     elif doesFileExist(os.path.join(folder,'stream1.mp2'))==True:
04004         write( "Audio track 2 is in mp2 format - re-encoding to ac3")
04005         encodeAudio("ac3",os.path.join(folder,'stream1.mp2'), os.path.join(folder,'stream1.ac3'),True)
04006     elif doesFileExist(os.path.join(folder,'stream1.mpa'))==True:
04007         write( "Audio track 2 is in mpa format - re-encoding to ac3")
04008         encodeAudio("ac3",os.path.join(folder,'stream1.mpa'), os.path.join(folder,'stream1.ac3'),True)
04009     elif doesFileExist(os.path.join(folder,'stream1.ac3'))==True:
04010         write( "Audio is already in ac3 format")
04011 
04012 #############################################################
04013 # chooses which streams from a file to include on the DVD
04014 
04015 # tuple index constants
04016 VIDEO_INDEX = 0
04017 VIDEO_CODEC = 1
04018 VIDEO_ID    = 2
04019 
04020 AUDIO_INDEX = 0
04021 AUDIO_CODEC = 1
04022 AUDIO_ID    = 2
04023 AUDIO_LANG  = 3
04024 
04025 def selectStreams(folder):
04026     """Choose the streams we want from the source file"""
04027 
04028     video    = (-1, 'N/A', -1)         # index, codec, ID
04029     audio1   = (-1, 'N/A', -1, 'N/A')  # index, codec, ID, lang
04030     audio2   = (-1, 'N/A', -1, 'N/A')
04031 
04032     #open the XML containing information about this file
04033     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
04034     #error out if its the wrong XML
04035     if infoDOM.documentElement.tagName != "file":
04036         fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
04037 
04038 
04039     #get video ID, CODEC
04040     nodes = infoDOM.getElementsByTagName("video")
04041     if nodes.length == 0:
04042         write("Didn't find any video elements in stream info file.!!!")
04043         write("");
04044         sys.exit(1)
04045     if nodes.length > 1:
04046         write("Found more than one video element in stream info file.!!!")
04047     node = nodes[0]
04048     video = (int(node.attributes["ffmpegindex"].value), node.attributes["codec"].value, int(node.attributes["id"].value))
04049 
04050     #get audioID's - we choose the best 2 audio streams using this algorithm
04051     # 1. if there is one or more stream(s) using the 1st preferred language we use that
04052     # 2. if there is one or more stream(s) using the 2nd preferred language we use that
04053     # 3. if we still haven't found a stream we use the stream with the lowest PID
04054     # 4. we prefer ac3 over mp2
04055     # 5. if there are more than one stream with the chosen language we use the one with the lowest PID
04056 
04057     write("Preferred audio languages %s and %s" % (preferredlang1, preferredlang2))
04058 
04059     nodes = infoDOM.getElementsByTagName("audio")
04060 
04061     if nodes.length == 0:
04062         write("Didn't find any audio elements in stream info file.!!!")
04063         write("");
04064         sys.exit(1)
04065 
04066     found = False
04067     # first try to find a stream with ac3 and preferred language 1
04068     for node in nodes:
04069         index = int(node.attributes["ffmpegindex"].value)
04070         lang = node.attributes["language"].value
04071         format = string.upper(node.attributes["codec"].value)
04072         pid = int(node.attributes["id"].value)
04073         if lang == preferredlang1 and format == "AC3":
04074             if found:
04075                 if pid < audio1[AUDIO_ID]:
04076                     audio1 = (index, format, pid, lang)
04077             else:
04078                 audio1 = (index, format, pid, lang)
04079             found = True
04080 
04081     # second try to find a stream with mp2 and preferred language 1
04082     if not found:
04083         for node in nodes:
04084             index = int(node.attributes["ffmpegindex"].value)
04085             lang = node.attributes["language"].value
04086             format = string.upper(node.attributes["codec"].value)
04087             pid = int(node.attributes["id"].value)
04088             if lang == preferredlang1 and format == "MP2":
04089                 if found:
04090                     if pid < audio1[AUDIO_ID]:
04091                         audio1 = (index, format, pid, lang)
04092                 else:
04093                     audio1 = (index, format, pid, lang)
04094                 found = True
04095 
04096     # finally use the stream with the lowest pid, prefer ac3 over mp2
04097     if not found:
04098         for node in nodes:
04099             index = int(node.attributes["ffmpegindex"].value)
04100             format = string.upper(node.attributes["codec"].value)
04101             pid = int(node.attributes["id"].value)
04102             if not found:
04103                 audio1 = (index, format, pid, lang)
04104                 found = True
04105             else:
04106                 if format == "AC3" and audio1[AUDIO_CODEC] == "MP2":
04107                     audio1 = (index, format, pid, lang)
04108                 else:
04109                     if pid < audio1[AUDIO_ID]:
04110                         audio1 = (index, format, pid, lang)
04111 
04112     # do we need to find a second audio stream?
04113     if preferredlang1 != preferredlang2 and nodes.length > 1:
04114         found = False
04115         # first try to find a stream with ac3 and preferred language 2
04116         for node in nodes:
04117             index = int(node.attributes["ffmpegindex"].value)
04118             lang = node.attributes["language"].value
04119             format = string.upper(node.attributes["codec"].value)
04120             pid = int(node.attributes["id"].value)
04121             if lang == preferredlang2 and format == "AC3":
04122                 if found:
04123                     if pid < audio2[AUDIO_ID]:
04124                         audio2 = (index, format, pid, lang)
04125                 else:
04126                     audio2 = (index, format, pid, lang)
04127                 found = True
04128 
04129         # second try to find a stream with mp2 and preferred language 2
04130         if not found:
04131             for node in nodes:
04132                 index = int(node.attributes["ffmpegindex"].value)
04133                 lang = node.attributes["language"].value
04134                 format = string.upper(node.attributes["codec"].value)
04135                 pid = int(node.attributes["id"].value)
04136                 if lang == preferredlang2 and format == "MP2":
04137                     if found:
04138                         if pid < audio2[AUDIO_ID]:
04139                             audio2 = (index, format, pid, lang)
04140                     else:
04141                         audio2 = (index, format, pid, lang)
04142                     found = True
04143 
04144         # finally use the stream with the lowest pid, prefer ac3 over mp2
04145         if not found:
04146             for node in nodes:
04147                 index = int(node.attributes["ffmpegindex"].value)
04148                 format = string.upper(node.attributes["codec"].value)
04149                 pid = int(node.attributes["id"].value)
04150                 if not found:
04151                     # make sure we don't choose the same stream as audio1
04152                     if pid != audio1[AUDIO_ID]:
04153                         audio2 = (index, format, pid, lang)
04154                         found = True
04155                 else:
04156                     if format == "AC3" and audio2[AUDIO_CODEC] == "MP2" and pid != audio1[AUDIO_ID]:
04157                         audio2 = (index, format, pid, lang)
04158                     else:
04159                         if pid < audio2[AUDIO_ID] and pid != audio1[AUDIO_ID]:
04160                             audio2 = (index, format, pid, lang)
04161 
04162     write("Video id: 0x%x, Audio1: [%d] 0x%x (%s, %s), Audio2: [%d] - 0x%x (%s, %s)" % \
04163         (video[VIDEO_ID], audio1[AUDIO_INDEX], audio1[AUDIO_ID], audio1[AUDIO_CODEC], audio1[AUDIO_LANG], \
04164          audio2[AUDIO_INDEX], audio2[AUDIO_ID], audio2[AUDIO_CODEC], audio2[AUDIO_LANG]))
04165 
04166     return (video, audio1, audio2)
04167 
04168 #############################################################
04169 # chooses which subtitle stream from a file to include on the DVD
04170 
04171 # tuple index constants
04172 SUBTITLE_INDEX = 0
04173 SUBTITLE_CODEC = 1
04174 SUBTITLE_ID    = 2
04175 SUBTITLE_LANG  = 3
04176 
04177 def selectSubtitleStream(folder):
04178     """Choose the subtitle stream we want from the source file"""
04179 
04180     subtitle   = (-1, 'N/A', -1, 'N/A')  # index, codec, ID, lang
04181 
04182     #open the XML containing information about this file
04183     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
04184     #error out if its the wrong XML
04185     if infoDOM.documentElement.tagName != "file":
04186         fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
04187 
04188 
04189     #get subtitle nodes
04190     nodes = infoDOM.getElementsByTagName("subtitle")
04191     if nodes.length == 0:
04192         write("Didn't find any subtitle elements in stream info file.")
04193         return subtitle
04194 
04195     write("Preferred languages %s and %s" % (preferredlang1, preferredlang2))
04196 
04197     found = False
04198     # first try to find a stream with preferred language 1
04199     for node in nodes:
04200         index = int(node.attributes["ffmpegindex"].value)
04201         lang = node.attributes["language"].value
04202         format = string.upper(node.attributes["codec"].value)
04203         pid = int(node.attributes["id"].value)
04204         if not found and lang == preferredlang1 and format == "dvbsub":
04205             subtitle = (index, format, pid, lang)
04206             found = True
04207 
04208     # second try to find a stream with preferred language 2
04209     if not found:
04210         for node in nodes:
04211             index = int(node.attributes["ffmpegindex"].value)
04212             lang = node.attributes["language"].value
04213             format = string.upper(node.attributes["codec"].value)
04214             pid = int(node.attributes["id"].value)
04215             if not found and lang == preferredlang2 and format == "dvbsub":
04216                 subtitle = (index, format, pid, lang)
04217                 found = True
04218 
04219     # finally use the first subtitle stream
04220     if not found:
04221         for node in nodes:
04222             index = int(node.attributes["ffmpegindex"].value)
04223             format = string.upper(node.attributes["codec"].value)
04224             pid = int(node.attributes["id"].value)
04225             if not found:
04226                 subtitle = (index, format, pid, lang)
04227                 found = True
04228 
04229     write("Subtitle id: 0x%x" % (subtitle[SUBTITLE_ID]))
04230 
04231     return subtitle
04232 
04233 #############################################################
04234 # gets the video aspect ratio from the stream info xml file
04235 
04236 def selectAspectRatio(folder):
04237     """figure out what aspect ratio we want from the source file"""
04238 
04239     #this should be smarter and look though the file for any AR changes
04240     #at the moment it just uses the AR found at the start of the file
04241 
04242     #open the XML containing information about this file
04243     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
04244     #error out if its the wrong XML
04245     if infoDOM.documentElement.tagName != "file":
04246         fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
04247 
04248 
04249     #get aspect ratio
04250     nodes = infoDOM.getElementsByTagName("video")
04251     if nodes.length == 0:
04252         write("Didn't find any video elements in stream info file.!!!")
04253         write("");
04254         sys.exit(1)
04255     if nodes.length > 1:
04256         write("Found more than one video element in stream info file.!!!")
04257     node = nodes[0]
04258     try:
04259         ar = float(node.attributes["aspectratio"].value)
04260         if ar > float(4.0/3.0 - 0.01) and ar < float(4.0/3.0 + 0.01):
04261             aspectratio = "4:3"
04262             write("Aspect ratio is 4:3")
04263         elif ar > float(16.0/9.0 - 0.01) and ar < float(16.0/9.0 + 0.01):
04264             aspectratio = "16:9"
04265             write("Aspect ratio is 16:9")
04266         else:
04267             write("Unknown aspect ratio %f - Using 16:9" % ar)
04268             aspectratio = "16:9"
04269     except:
04270         aspectratio = "16:9"
04271 
04272     return aspectratio
04273 
04274 #############################################################
04275 # gets video stream codec from the stream info xml file
04276 
04277 def getVideoCodec(folder):
04278     """Get the video codec from the streaminfo.xml for the file"""
04279 
04280     #open the XML containing information about this file
04281     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
04282     #error out if its the wrong XML
04283     if infoDOM.documentElement.tagName != "file":
04284         fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
04285 
04286     nodes = infoDOM.getElementsByTagName("video")
04287     if nodes.length == 0:
04288         write("Didn't find any video elements in stream info file!!!")
04289         write("");
04290         sys.exit(1)
04291     if nodes.length > 1:
04292         write("Found more than one video element in stream info file!!!")
04293     node = nodes[0]
04294     return node.attributes["codec"].value
04295 
04296 #############################################################
04297 # gets file container type from the stream info xml file
04298 
04299 def getFileType(folder):
04300     """Get the overall file type from the streaminfo.xml for the file"""
04301 
04302     #open the XML containing information about this file
04303     infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
04304     #error out if its the wrong XML
04305     if infoDOM.documentElement.tagName != "file":
04306         fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
04307 
04308     nodes = infoDOM.getElementsByTagName("file")
04309     if nodes.length == 0:
04310         write("Didn't find any file elements in stream info file!!!")
04311         write("");
04312         sys.exit(1)
04313     if nodes.length > 1:
04314         write("Found more than one file element in stream info file!!!")
04315     node = nodes[0]
04316 
04317     return node.attributes["type"].value
04318 
04319 #############################################################
04320 # get the list of required stream ids for a file
04321 
04322 def getStreamList(folder):
04323 
04324     # choose which streams we need
04325     video, audio1, audio2 = selectStreams(folder)
04326 
04327     streamList = "0x%x" % video[VIDEO_ID]
04328 
04329     if audio1[AUDIO_ID] != -1:
04330         streamList += ",0x%x" % audio1[AUDIO_ID]
04331 
04332     if audio2[AUDIO_ID] != -1:
04333         streamList += ",0x%x" % audio2[AUDIO_ID]
04334 
04335     # add subtitle stream id if required
04336     if addSubtitles:
04337         subtitles = selectSubtitleStream(folder)
04338         if subtitles[SUBTITLE_ID] != -1:
04339             streamList += ",0x%x" % subtitles[SUBTITLE_ID]
04340 
04341     return streamList;
04342 
04343 
04344 #############################################################
04345 # check if file is DVD compliant
04346 
04347 def isFileOkayForDVD(file, folder):
04348     """return true if the file is dvd compliant"""
04349 
04350     if string.lower(getVideoCodec(folder)) != "mpeg2video":
04351         return False
04352 
04353 #    if string.lower(getAudioCodec(folder)) != "ac3" and encodeToAC3:
04354 #        return False
04355 
04356     videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
04357 
04358     # has the user elected to re-encode the file
04359     if file.hasAttribute("encodingprofile"):
04360         if file.attributes["encodingprofile"].value != "NONE":
04361             write("File will be re-encoded using profile %s" % file.attributes["encodingprofile"].value)
04362             return False
04363 
04364     if not isResolutionOkayForDVD(videosize):
04365         # file does not have a dvd resolution
04366         if file.hasAttribute("encodingprofile"):
04367             if file.attributes["encodingprofile"].value == "NONE":
04368                 write("WARNING: File does not have a DVD compliant resolution but "
04369                       "you have selected not to re-encode the file")
04370                 return True
04371         else:
04372             return False
04373 
04374     return True
04375 
04376 #############################################################
04377 # process a single file ready for burning using either
04378 # mythtranscode/mythreplex or ProjectX as the cutter/demuxer
04379 
04380 def processFile(file, folder, count):
04381     """Process a single video/recording file ready for burning."""
04382 
04383     if useprojectx:
04384         doProcessFileProjectX(file, folder, count)
04385     else:
04386         doProcessFile(file, folder, count)
04387 
04388 #############################################################
04389 # process a single file ready for burning using mythtranscode/mythreplex
04390 # to cut and demux 
04391 
04392 def doProcessFile(file, folder, count):
04393     """Process a single video/recording file ready for burning."""
04394 
04395     write( "*************************************************************")
04396     write( "Processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
04397     write( "*************************************************************")
04398 
04399     #As part of this routine we need to pre-process the video this MAY mean:
04400     #1. removing commercials/cleaning up mpeg2 stream
04401     #2. encoding to mpeg2 (if its an avi for instance or isn't DVD compatible)
04402     #3. selecting audio track to use and encoding audio from mp2 into ac3
04403     #4. de-multiplexing into video and audio steams)
04404 
04405     mediafile=""
04406 
04407     if file.hasAttribute("localfilename"):
04408         mediafile=file.attributes["localfilename"].value
04409     elif file.attributes["type"].value=="recording":
04410         mediafile = file.attributes["filename"].value
04411     elif file.attributes["type"].value=="video":
04412         mediafile=os.path.join(videopath, file.attributes["filename"].value)
04413     elif file.attributes["type"].value=="file":
04414         mediafile=file.attributes["filename"].value
04415     else:
04416         fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
04417 
04418     #Get the XML containing information about this item
04419     infoDOM = xml.dom.minidom.parse( os.path.join(folder,"info.xml") )
04420     #Error out if its the wrong XML
04421     if infoDOM.documentElement.tagName != "fileinfo":
04422         fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
04423 
04424     #If this is an mpeg2 myth recording and there is a cut list available and the user wants to use it
04425     #run mythtranscode to cut out commercials etc
04426     if file.attributes["type"].value == "recording":
04427         #can only use mythtranscode to cut commercials on mpeg2 files
04428         write("File type is '%s'" % getFileType(folder))
04429         write("Video codec is '%s'" % getVideoCodec(folder))
04430         if string.lower(getVideoCodec(folder)) == "mpeg2video": 
04431             if file.attributes["usecutlist"].value == "1" and getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes":
04432                 # Run from local file?
04433                 if file.hasAttribute("localfilename"):
04434                     localfile = file.attributes["localfilename"].value
04435                 else:
04436                     localfile = ""
04437                 write("File has a cut list - running mythtranscode to remove unwanted segments")
04438                 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04439                 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04440                 if runMythtranscode(chanid, starttime, os.path.join(folder,'newfile.mpg'), True, localfile):
04441                     mediafile = os.path.join(folder,'newfile.mpg')
04442                 else:
04443                     write("Failed to run mythtranscode to remove unwanted segments")
04444             else:
04445                 #does the user always want to run recordings through mythtranscode?
04446                 #may help to fix any errors in the file 
04447                 if (alwaysRunMythtranscode == True or 
04448                         (getFileType(folder) == "mpegts" and isFileOkayForDVD(file, folder))):
04449                     # Run from local file?
04450                     if file.hasAttribute("localfilename"):
04451                         localfile = file.attributes["localfilename"].value
04452                     else:
04453                         localfile = ""
04454                     write("Running mythtranscode --mpeg2 to fix any errors")
04455                     chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04456                     starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04457                     if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
04458                         mediafile = os.path.join(folder, 'newfile.mpg')
04459                     else:
04460                         write("Failed to run mythtranscode to fix any errors")
04461     else:
04462         #does the user always want to run mpeg2 files through mythtranscode?
04463         #may help to fix any errors in the file 
04464         write("File type is '%s'" % getFileType(folder))
04465         write("Video codec is '%s'" % getVideoCodec(folder))
04466 
04467         if (alwaysRunMythtranscode == True and 
04468                 string.lower(getVideoCodec(folder)) == "mpeg2video" and
04469                 isFileOkayForDVD(file, folder)):
04470             if file.hasAttribute("localfilename"):
04471                 localfile = file.attributes["localfilename"].value
04472             else:
04473                 localfile = file.attributes["filename"].value
04474             write("Running mythtranscode --mpeg2 to fix any errors")
04475             chanid = -1
04476             starttime = -1
04477             if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
04478                 mediafile = os.path.join(folder, 'newfile.mpg')
04479             else:
04480                 write("Failed to run mythtranscode to fix any errors")
04481 
04482     #do we need to re-encode the file to make it DVD compliant?
04483     if not isFileOkayForDVD(file, folder):
04484         if getFileType(folder) == 'nuv':
04485             #file is a nuv file which mythffmpeg has problems reading so use mythtranscode to pass
04486             #the video and audio streams to mythffmpeg to do the reencode
04487 
04488             #we need to re-encode the file, make sure we get the right video/audio streams
04489             #would be good if we could also split the file at the same time
04490             getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
04491 
04492             #choose which streams we need
04493             video, audio1, audio2 = selectStreams(folder)
04494 
04495             #choose which aspect ratio we should use
04496             aspectratio = selectAspectRatio(folder)
04497 
04498             write("Re-encoding audio and video from nuv file")
04499 
04500             # what encoding profile should we use
04501             if file.hasAttribute("encodingprofile"):
04502                 profile = file.attributes["encodingprofile"].value
04503             else:
04504                 profile = defaultEncodingProfile
04505 
04506             if file.hasAttribute("localfilename"):
04507                 mediafile = file.attributes["localfilename"].value
04508                 chanid = -1
04509                 starttime = -1
04510                 usecutlist = -1
04511             elif file.attributes["type"].value == "recording":
04512                 mediafile = -1
04513                 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04514                 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04515                 usecutlist = (file.attributes["usecutlist"].value == "1" and 
04516                             getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes")
04517             else:
04518                 chanid = -1
04519                 starttime = -1
04520                 usecutlist = -1
04521 
04522             encodeNuvToMPEG2(chanid, starttime, mediafile, os.path.join(folder, "newfile2.mpg"), folder,
04523                          profile, usecutlist)
04524             mediafile = os.path.join(folder, 'newfile2.mpg')
04525         else:
04526             #we need to re-encode the file, make sure we get the right video/audio streams
04527             #would be good if we could also split the file at the same time
04528             getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
04529 
04530             #choose which streams we need
04531             video, audio1, audio2 = selectStreams(folder)
04532 
04533             #choose which aspect ratio we should use
04534             aspectratio = selectAspectRatio(folder)
04535 
04536             write("Re-encoding audio and video")
04537 
04538             # Run from local file?
04539             if file.hasAttribute("localfilename"):
04540                 mediafile = file.attributes["localfilename"].value
04541 
04542             # what encoding profile should we use
04543             if file.hasAttribute("encodingprofile"):
04544                 profile = file.attributes["encodingprofile"].value
04545             else:
04546                 profile = defaultEncodingProfile
04547 
04548             #do the re-encode 
04549             encodeVideoToMPEG2(mediafile, os.path.join(folder, "newfile2.mpg"), video,
04550                             audio1, audio2, aspectratio, profile)
04551             mediafile = os.path.join(folder, 'newfile2.mpg')
04552 
04553             #remove the old mediafile that was run through mythtranscode
04554             #if it exists
04555             if debug_keeptempfiles==False:
04556                 if os.path.exists(os.path.join(folder, "newfile.mpg")):
04557                     os.remove(os.path.join(folder,'newfile.mpg'))
04558 
04559     # the file is now DVD compliant split it into video and audio parts
04560 
04561     # find out what streams we have available now
04562     getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 1)
04563 
04564     # choose which streams we need
04565     video, audio1, audio2 = selectStreams(folder)
04566 
04567     # now attempt to split the source file into video and audio parts
04568     write("Splitting MPEG stream into audio and video parts")
04569     deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2)
04570 
04571     # remove intermediate files
04572     if debug_keeptempfiles==False:
04573         if os.path.exists(os.path.join(folder, "newfile.mpg")):
04574             os.remove(os.path.join(folder,'newfile.mpg'))
04575         if os.path.exists(os.path.join(folder, "newfile2.mpg")):
04576             os.remove(os.path.join(folder,'newfile2.mpg'))
04577 
04578     # we now have a video stream and one or more audio streams
04579     # check if we need to convert any of the audio streams to ac3
04580     processAudio(folder)
04581 
04582     # if we don't already have one find a title thumbnail image
04583     titleImage = os.path.join(folder, "title.jpg")
04584     if not os.path.exists(titleImage):
04585         # if the file is a recording try to use its preview image for the thumb
04586         if file.attributes["type"].value == "recording":
04587             previewImage = file.attributes["filename"].value + ".png"
04588             if usebookmark == True and os.path.exists(previewImage):
04589                 copy(previewImage, titleImage)
04590             else:
04591                 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
04592         else:
04593             extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
04594 
04595     write( "*************************************************************")
04596     write( "Finished processing '%s'" % file.attributes["filename"].value)
04597     write( "*************************************************************")
04598 
04599 
04600 #############################################################
04601 # process a single file ready for burning using projectX to
04602 # cut and demux
04603 
04604 def doProcessFileProjectX(file, folder, count):
04605     """Process a single video/recording file ready for burning."""
04606 
04607     write( "*************************************************************")
04608     write( "Processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
04609     write( "*************************************************************")
04610 
04611     #As part of this routine we need to pre-process the video this MAY mean:
04612     #1. encoding to mpeg2 (if its an avi for instance or isn't DVD compatible)
04613     #2. removing commercials/cleaning up mpeg2 stream 
04614     #3. selecting audio track(s) to use and encoding audio from mp2 into ac3
04615     #4. de-multiplexing into video and audio steams
04616 
04617     mediafile=""
04618 
04619     if file.hasAttribute("localfilename"):
04620         mediafile=file.attributes["localfilename"].value
04621     elif file.attributes["type"].value=="recording":
04622         mediafile = file.attributes["filename"].value
04623     elif file.attributes["type"].value=="video":
04624         mediafile=os.path.join(videopath, file.attributes["filename"].value)
04625     elif file.attributes["type"].value=="file":
04626         mediafile=file.attributes["filename"].value
04627     else:
04628         fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
04629 
04630     #Get the XML containing information about this item
04631     infoDOM = xml.dom.minidom.parse( os.path.join(folder,"info.xml") )
04632     #Error out if its the wrong XML
04633     if infoDOM.documentElement.tagName != "fileinfo":
04634         fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
04635 
04636     #do we need to re-encode the file to make it DVD compliant?
04637     if not isFileOkayForDVD(file, folder):
04638         if getFileType(folder) == 'nuv':
04639             #file is a nuv file which mythffmpeg has problems reading so use mythtranscode to pass
04640             #the video and audio streams to mythffmpeg to do the reencode
04641 
04642             #we need to re-encode the file, make sure we get the right video/audio streams
04643             #would be good if we could also split the file at the same time
04644             getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
04645 
04646             #choose which streams we need
04647             video, audio1, audio2 = selectStreams(folder)
04648 
04649             #choose which aspect ratio we should use
04650             aspectratio = selectAspectRatio(folder)
04651 
04652             write("Re-encoding audio and video from nuv file")
04653 
04654             # what encoding profile should we use
04655             if file.hasAttribute("encodingprofile"):
04656                 profile = file.attributes["encodingprofile"].value
04657             else:
04658                 profile = defaultEncodingProfile
04659 
04660             if file.hasAttribute("localfilename"):
04661                 mediafile = file.attributes["localfilename"].value
04662                 chanid = -1
04663                 starttime = -1
04664                 usecutlist = -1
04665             elif file.attributes["type"].value == "recording":
04666                 mediafile = -1
04667                 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04668                 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04669                 usecutlist = (file.attributes["usecutlist"].value == "1" and 
04670                             getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes")
04671             else:
04672                 chanid = -1
04673                 starttime = -1
04674                 usecutlist = -1
04675 
04676             encodeNuvToMPEG2(chanid, starttime, mediafile, os.path.join(folder, "newfile2.mpg"), folder,
04677                          profile, usecutlist)
04678             mediafile = os.path.join(folder, 'newfile2.mpg')
04679         else:
04680             #we need to re-encode the file, make sure we get the right video/audio streams
04681             #would be good if we could also split the file at the same time
04682             getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
04683 
04684             #choose which streams we need
04685             video, audio1, audio2 = selectStreams(folder)
04686 
04687             #choose which aspect ratio we should use
04688             aspectratio = selectAspectRatio(folder)
04689 
04690             write("Re-encoding audio and video")
04691 
04692             # Run from local file?
04693             if file.hasAttribute("localfilename"):
04694                 mediafile = file.attributes["localfilename"].value
04695 
04696             # what encoding profile should we use
04697             if file.hasAttribute("encodingprofile"):
04698                 profile = file.attributes["encodingprofile"].value
04699             else:
04700                 profile = defaultEncodingProfile
04701 
04702             #do the re-encode 
04703             encodeVideoToMPEG2(mediafile, os.path.join(folder, "newfile2.mpg"), video,
04704                             audio1, audio2, aspectratio, profile)
04705             mediafile = os.path.join(folder, 'newfile2.mpg')
04706 
04707     #remove an intermediate file
04708     if os.path.exists(os.path.join(folder, "newfile1.mpg")):
04709         os.remove(os.path.join(folder,'newfile1.mpg'))
04710 
04711     # the file is now DVD compliant now we need to remove commercials
04712     # and split it into video, audio, subtitle parts
04713 
04714     # find out what streams we have available now
04715     getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 1)
04716 
04717     # choose which streams we need
04718     video, audio1, audio2 = selectStreams(folder)
04719 
04720     # now attempt to split the source file into video and audio parts
04721     # using projectX
04722 
04723     # If this is an mpeg2 myth recording and there is a cut list available and the 
04724     # user wants to use it run projectx to cut out commercials etc
04725     if file.attributes["type"].value == "recording":
04726         if file.attributes["usecutlist"].value == "1" and getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes":
04727             chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04728             starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04729             write("File has a cut list - running Project-X to remove unwanted segments")
04730             if not runProjectX(chanid, starttime, folder, True, mediafile):
04731                 fatalError("Failed to run Project-X to remove unwanted segments and demux")
04732         else:
04733             # no cutlist so just demux this file
04734             chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
04735             starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
04736             write("Using Project-X to demux file")
04737             if not runProjectX(chanid, starttime, folder, False, mediafile):
04738                 fatalError("Failed to run Project-X to demux file")
04739     else:
04740         # just demux this file
04741         chanid = -1
04742         starttime = -1
04743         write("Running Project-X to demux file")
04744         if not runProjectX(chanid, starttime, folder, False, mediafile):
04745             fatalError("Failed to run Project-X to demux file")
04746 
04747     # we now have a video stream and one or more audio streams
04748     # check if we need to convert any of the audio streams to ac3
04749     processAudio(folder)
04750 
04751     # if we don't already have one find a title thumbnail image
04752     titleImage = os.path.join(folder, "title.jpg")
04753     if not os.path.exists(titleImage):
04754         # if the file is a recording try to use its preview image for the thumb
04755         if file.attributes["type"].value == "recording":
04756             previewImage = file.attributes["filename"].value + ".png"
04757             if usebookmark == True and os.path.exists(previewImage):
04758                 copy(previewImage, titleImage)
04759             else:
04760                 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
04761         else:
04762             extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
04763 
04764     write( "*************************************************************")
04765     write( "Finished processing file '%s'" % file.attributes["filename"].value)
04766     write( "*************************************************************")
04767 
04768 #############################################################
04769 # copy files on remote filesystems to the local filesystem
04770 
04771 def copyRemote(files, tmpPath):
04772     '''go through the list of files looking for files on remote filesytems
04773        and copy them to a local file for quicker processing'''
04774     localTmpPath = os.path.join(tmpPath, "localcopy")
04775     for node in files:
04776         tmpfile = node.attributes["filename"].value
04777         filename = os.path.basename(tmpfile)
04778 
04779         res = runCommand("mytharchivehelper -q -q --isremote --infile " + quoteCmdArg(tmpfile))
04780         if res == 2:
04781             # file is on a remote filesystem so copy it to a local file
04782             write("Copying file from " + tmpfile)
04783             write("to " + os.path.join(localTmpPath, filename))
04784 
04785             # Copy file
04786             if not doesFileExist(os.path.join(localTmpPath, filename)):
04787                 copy(tmpfile, os.path.join(localTmpPath, filename))
04788 
04789             # update node
04790             node.setAttribute("localfilename", os.path.join(localTmpPath, filename))
04791     return files
04792 
04793 #############################################################
04794 # processes one job
04795 
04796 def processJob(job):
04797     """Starts processing a MythBurn job, expects XML nodes to be passed as input."""
04798     global wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage
04799     global themeDOM, themeName, themeFonts
04800 
04801 
04802     media=job.getElementsByTagName("media")
04803 
04804     if media.length==1:
04805 
04806         themeName=job.attributes["theme"].value
04807 
04808         #Check theme exists
04809         if not validateTheme(themeName):
04810             fatalError("Failed to validate theme (%s)" % themeName)
04811         #Get the theme XML
04812         themeDOM = getThemeConfigurationXML(themeName)
04813 
04814         #Pre generate all the fonts we need
04815         loadFonts(themeDOM)
04816 
04817         #Update the global flags
04818         nodes=themeDOM.getElementsByTagName("intro")
04819         wantIntro = (nodes.length > 0)
04820 
04821         nodes=themeDOM.getElementsByTagName("menu")
04822         wantMainMenu = (nodes.length > 0)
04823 
04824         nodes=themeDOM.getElementsByTagName("submenu")
04825         wantChapterMenu = (nodes.length > 0)
04826 
04827         nodes=themeDOM.getElementsByTagName("detailspage")
04828         wantDetailsPage = (nodes.length > 0)
04829 
04830         write( "wantIntro: %d, wantMainMenu: %d, wantChapterMenu: %d, wantDetailsPage: %d" \
04831                 % (wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage))
04832 
04833         if videomode=="ntsc":
04834             format=dvdNTSC
04835             dpi=dvdNTSCdpi
04836         elif videomode=="pal":
04837             format=dvdPAL
04838             dpi=dvdPALdpi
04839         else:
04840             fatalError("Unknown videomode is set (%s)" % videomode)
04841 
04842         write( "Final DVD Video format will be " + videomode)
04843 
04844 
04845         #Loop through all the files
04846         files=media[0].getElementsByTagName("file")
04847         filecount=0
04848         if files.length > 0:
04849             write( "There are %s file(s) to process" % files.length)
04850 
04851             if debug_secondrunthrough==False:
04852                 #Delete all the temporary files that currently exist
04853                 deleteEverythingInFolder(getTempPath())
04854 
04855             #If User wants to, copy remote files to a tmp dir
04856             if copyremoteFiles==True:
04857                 if debug_secondrunthrough==False:
04858                     localCopyFolder=os.path.join(getTempPath(),"localcopy")
04859                     os.makedirs(localCopyFolder)
04860                 files=copyRemote(files,getTempPath())
04861 
04862             #First pass through the files to be recorded - sense check
04863             #we dont want to find half way through this long process that
04864             #a file does not exist, or is the wrong format!!
04865             for node in files:
04866                 filecount+=1
04867 
04868                 #Generate a temp folder name for this file
04869                 folder=getItemTempPath(filecount)
04870 
04871                 if debug_secondrunthrough==False:
04872                     os.makedirs(folder)
04873                 #Do the pre-process work
04874                 preProcessFile(node,folder,filecount)
04875 
04876             if debug_secondrunthrough==False:
04877                 #Loop through all the files again but this time do more serious work!
04878                 filecount=0
04879                 for node in files:
04880                     filecount+=1
04881                     folder=getItemTempPath(filecount)
04882 
04883                     #Process this file
04884                     processFile(node,folder,filecount)
04885 
04886             #We can only create the menus after the videos have been processed
04887             #and the commercials cut out so we get the correct run time length
04888             #for the chapter marks and thumbnails.
04889             #create the DVD menus...
04890             if wantMainMenu:
04891                 createMenu(format, dpi, files.length)
04892 
04893             #Submenus are visible when you select the chapter menu while the recording is playing
04894             if wantChapterMenu:
04895                 createChapterMenu(format, dpi, files.length)
04896 
04897             #Details Page are displayed just before playing each recording
04898             if wantDetailsPage:
04899                 createDetailsPage(format, dpi, files.length)
04900 
04901             #DVD Author file
04902             if not wantMainMenu and not wantChapterMenu:
04903                 createDVDAuthorXMLNoMenus(format, files.length)
04904             elif not wantMainMenu:
04905                 createDVDAuthorXMLNoMainMenu(format, files.length)
04906             else:
04907                 createDVDAuthorXML(format, files.length)
04908 
04909             #Check all the files will fit onto a recordable DVD
04910             if mediatype == DVD_DL:
04911                 # dual layer
04912                 performMPEG2Shrink(files, dvdrsize[1])
04913             else:
04914                 #single layer
04915                 performMPEG2Shrink(files, dvdrsize[0])
04916 
04917             filecount=0
04918             for node in files:
04919                 filecount+=1
04920                 folder=getItemTempPath(filecount)
04921                 #Multiplex this file
04922                 #(This also removes non-required audio feeds inside mpeg streams 
04923                 #(through re-multiplexing) we only take 1 video and 1 or 2 audio streams)
04924                 pid=multiplexMPEGStream(os.path.join(folder,'stream.mv2'),
04925                         os.path.join(folder,'stream0'),
04926                         os.path.join(folder,'stream1'),
04927                         os.path.join(folder,'final.vob'),
04928                         calcSyncOffset(filecount))
04929 
04930             #Now all the files are completed and ready to be burnt
04931             runDVDAuthor()
04932 
04933             #Delete dvdauthor work files
04934             if debug_keeptempfiles==False:
04935                 filecount=0
04936                 for node in files:
04937                     filecount+=1
04938                     folder=getItemTempPath(filecount)
04939                     if os.path.exists(os.path.join(folder, "stream.mv2")):
04940                         os.remove(os.path.join(folder,'stream.mv2'))
04941                     if os.path.exists(os.path.join(folder, "stream0.mp2")):
04942                         os.remove(os.path.join(folder,'stream0.mp2'))
04943                     if os.path.exists(os.path.join(folder, "stream1.mp2")):
04944                         os.remove(os.path.join(folder,'stream1.mp2'))
04945                     if os.path.exists(os.path.join(folder, "stream0.ac3")):
04946                         os.remove(os.path.join(folder,'stream0.ac3'))
04947                     if os.path.exists(os.path.join(folder, "stream1.ac3")):
04948                         os.remove(os.path.join(folder,'stream1.ac3'))
04949 
04950             #Get DVD title from first processed file
04951             #Get the XML containing information about this item
04952             infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(1),"info.xml") )
04953             #Error out if its the wrong XML
04954             if infoDOM.documentElement.tagName != "fileinfo":
04955                 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
04956             title = expandItemText(infoDOM,"%title",1,0,0,0,0)
04957 
04958             # replace all non-ascii-characters
04959             title.encode('ascii', 'replace').decode('ascii', 'replace')
04960             title.strip()
04961             # replace not-allowed characters
04962             index = 0
04963             title_new = ''
04964             while (index < len(title)) and (index<=7):
04965                 if title[index].isalnum and title[index] != ' ':
04966                     title_new += title[index]
04967                 else:
04968                     title_new += '_'
04969                 index = index + 1
04970             title = title_new.upper()
04971             if len(title) < 1:
04972                 title = 'UNNAMED'
04973 
04974             #Create the DVD ISO image
04975             if docreateiso == True or mediatype == FILE:
04976                 CreateDVDISO(title)
04977 
04978             #Burn the DVD ISO image
04979             if doburn == True and mediatype != FILE:
04980                 BurnDVDISO(title)
04981 
04982             #Move the created iso image to the given location
04983             if mediatype == FILE and savefilename != "":
04984                 write("Moving ISO image to: %s" % savefilename)
04985                 try:
04986                     os.rename(os.path.join(getTempPath(), 'mythburn.iso'), savefilename)
04987                 except:
04988                     f1 = open(os.path.join(getTempPath(), 'mythburn.iso'), 'rb')
04989                     f2 = open(savefilename, 'wb')
04990                     data = f1.read(1024 * 1024)
04991                     while data:
04992                         f2.write(data)
04993                         data = f1.read(1024 * 1024)
04994                     f1.close()
04995                     f2.close()
04996                     os.unlink(os.path.join(getTempPath(), 'mythburn.iso'))
04997         else:
04998             write( "Nothing to do! (files)")
04999     else:
05000         write( "Nothing to do! (media)")
05001     return
05002 
05003 #############################################################
05004 # show usage
05005 
05006 def usage():
05007     write("""
05008     -h/--help               (Show this usage)
05009     -j/--jobfile file       (use file as the job file)
05010     -l/--progresslog file   (log file to output progress messages)
05011 
05012     """)
05013 
05014 #############################################################
05015 # The main starting point for mythburn.py
05016 
05017 def main():
05018     global sharepath, scriptpath, cpuCount, videopath, gallerypath, musicpath
05019     global videomode, temppath, logpath, dvddrivepath, dbVersion, preferredlang1
05020     global preferredlang2, useFIFO, encodetoac3, alwaysRunMythtranscode
05021     global copyremoteFiles, mainmenuAspectRatio, chaptermenuAspectRatio, dateformat
05022     global timeformat, clearArchiveTable, nicelevel, drivespeed, path_mplex
05023     global path_dvdauthor, path_mkisofs, path_growisofs, path_M2VRequantiser, addSubtitles
05024     global path_jpeg2yuv, path_spumux, path_mpeg2enc, path_projectx, useprojectx, progresslog
05025     global progressfile, jobfile
05026 
05027     write( "mythburn.py (%s) starting up..." % VERSION)
05028 
05029     #Ensure we are running at least python 2.3.5
05030     if not hasattr(sys, "hexversion") or sys.hexversion < 0x20305F0:
05031         sys.stderr.write("Sorry, your Python is too old. Please upgrade at least to 2.3.5\n")
05032         sys.exit(1)
05033 
05034     # figure out where this script is located
05035     scriptpath = os.path.dirname(sys.argv[0])
05036     scriptpath = os.path.abspath(scriptpath)
05037     write("script path:" + scriptpath)
05038 
05039     # figure out where the myth share directory is located
05040     sharepath = os.path.split(scriptpath)[0]
05041     sharepath = os.path.split(sharepath)[0]
05042     write("myth share path:" + sharepath)
05043 
05044     # process any command line options
05045     try:
05046         opts, args = getopt.getopt(sys.argv[1:], "j:hl:", ["jobfile=", "help", "progresslog="])
05047     except getopt.GetoptError:
05048         # print usage and exit
05049         usage()
05050         sys.exit(2)
05051 
05052     for o, a in opts:
05053         if o in ("-h", "--help"):
05054             usage()
05055             sys.exit()
05056         if o in ("-j", "--jobfile"):
05057             jobfile = str(a)
05058             write("passed job file: " + a)
05059         if o in ("-l", "--progresslog"):
05060             progresslog = str(a)
05061             write("passed progress log file: " + a)
05062 
05063     #if we have been given a progresslog filename to write to open it
05064     if progresslog != "":
05065         if os.path.exists(progresslog):
05066             os.remove(progresslog)
05067         progressfile = open(progresslog, 'w')
05068         write( "mythburn.py (%s) starting up..." % VERSION)
05069 
05070     #Get mysql database parameters
05071     #getMysqlDBParameters()
05072 
05073     saveSetting("MythArchiveLastRunStart", time.strftime("%Y-%m-%d %H:%M:%S "))
05074     saveSetting("MythArchiveLastRunType", "DVD")
05075     saveSetting("MythArchiveLastRunStatus", "Running")
05076 
05077     cpuCount = getCPUCount()
05078 
05079     #if the script is run from the web interface the PATH environment variable does not include
05080     #many of the bin locations we need so just append a few likely locations where our required
05081     #executables may be
05082     if not os.environ['PATH'].endswith(':'):
05083         os.environ['PATH'] += ":"
05084     os.environ['PATH'] += "/bin:/sbin:/usr/local/bin:/usr/bin:/opt/bin:" + installPrefix +"/bin:"
05085 
05086     #Get defaults from MythTV database
05087     defaultsettings = getDefaultParametersFromMythTVDB()
05088     videopath = defaultsettings.get("VideoStartupDir", None)
05089     gallerypath = defaultsettings.get("GalleryDir", None)
05090     musicpath = defaultsettings.get("MusicLocation", None)
05091     videomode = string.lower(defaultsettings["MythArchiveVideoFormat"])
05092     temppath = os.path.join(defaultsettings["MythArchiveTempDir"], "work")
05093     logpath = os.path.join(defaultsettings["MythArchiveTempDir"], "logs")
05094     write("temppath: " + temppath)
05095     write("logpath:  " + logpath)
05096     dvddrivepath = defaultsettings["MythArchiveDVDLocation"]
05097     dbVersion = defaultsettings["DBSchemaVer"]
05098     preferredlang1 = defaultsettings["ISO639Language0"]
05099     preferredlang2 = defaultsettings["ISO639Language1"]
05100     useFIFO = (defaultsettings["MythArchiveUseFIFO"] == '1')
05101     alwaysRunMythtranscode = (defaultsettings["MythArchiveAlwaysUseMythTranscode"] == '1')
05102     copyremoteFiles = (defaultsettings["MythArchiveCopyRemoteFiles"] == '1')
05103     mainmenuAspectRatio = defaultsettings["MythArchiveMainMenuAR"]
05104     chaptermenuAspectRatio = defaultsettings["MythArchiveChapterMenuAR"]
05105     dateformat = defaultsettings.get("MythArchiveDateFormat", "%a %d %b %Y")
05106     timeformat = defaultsettings.get("MythArchiveTimeFormat", "%I:%M %p")
05107     drivespeed = int(defaultsettings.get("MythArchiveDriveSpeed", "0"))
05108     if "MythArchiveClearArchiveTable" in defaultsettings:
05109         clearArchiveTable = (defaultsettings["MythArchiveClearArchiveTable"] == '1')
05110     nicelevel = defaultsettings.get("JobQueueCPU", "0")
05111 
05112     # external commands
05113     path_mplex = [defaultsettings["MythArchiveMplexCmd"], os.path.split(defaultsettings["MythArchiveMplexCmd"])[1]]
05114     path_dvdauthor = [defaultsettings["MythArchiveDvdauthorCmd"], os.path.split(defaultsettings["MythArchiveDvdauthorCmd"])[1]]
05115     path_mkisofs = [defaultsettings["MythArchiveMkisofsCmd"], os.path.split(defaultsettings["MythArchiveMkisofsCmd"])[1]]
05116     path_growisofs = [defaultsettings["MythArchiveGrowisofsCmd"], os.path.split(defaultsettings["MythArchiveGrowisofsCmd"])[1]]
05117     path_M2VRequantiser = [defaultsettings["MythArchiveM2VRequantiserCmd"], os.path.split(defaultsettings["MythArchiveM2VRequantiserCmd"])[1]]
05118     path_jpeg2yuv = [defaultsettings["MythArchiveJpeg2yuvCmd"], os.path.split(defaultsettings["MythArchiveJpeg2yuvCmd"])[1]]
05119     path_spumux = [defaultsettings["MythArchiveSpumuxCmd"], os.path.split(defaultsettings["MythArchiveSpumuxCmd"])[1]]
05120     path_mpeg2enc = [defaultsettings["MythArchiveMpeg2encCmd"], os.path.split(defaultsettings["MythArchiveMpeg2encCmd"])[1]]
05121 
05122     path_projectx = [defaultsettings["MythArchiveProjectXCmd"], os.path.split(defaultsettings["MythArchiveProjectXCmd"])[1]]
05123     useprojectx = (defaultsettings["MythArchiveUseProjectX"] == '1')
05124     addSubtitles = (defaultsettings["MythArchiveAddSubtitles"] == '1')
05125 
05126     # sanity check
05127     if path_projectx[0] == "":
05128         useprojectx = False
05129 
05130     if nicelevel == '1':
05131         nicelevel = 10
05132     elif nicelevel == '2':
05133         nicelevel = 0
05134     else:
05135         nicelevel = 17
05136 
05137     nicelevel = os.nice(nicelevel)
05138     write( "Setting process priority to %s" % nicelevel)
05139 
05140     import errno
05141 
05142     try:
05143         # Attempt to create a lock file so any UI knows we are running.
05144         # Testing for and creation of the lock is one atomic operation.
05145         lckpath = os.path.join(logpath, "mythburn.lck")
05146         try:
05147             fd = os.open(lckpath, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
05148             try:
05149                 os.write(fd, "%d\n" % os.getpid())
05150                 os.close(fd)
05151             except:
05152                 os.remove(lckpath)
05153                 raise
05154         except OSError, e:
05155             if e.errno == errno.EEXIST:
05156                 write("Lock file exists -- already running???")
05157                 sys.exit(1)
05158             else:
05159                 fatalError("cannot create lockfile: %s" % e)
05160         # if we get here, we own the lock
05161 
05162         try:
05163             #Load XML input file from disk
05164             jobDOM = xml.dom.minidom.parse(jobfile)
05165 
05166             #Error out if its the wrong XML
05167             if jobDOM.documentElement.tagName != "mythburn":
05168                 fatalError("Job file doesn't look right!")
05169 
05170             #process each job
05171             jobcount=0
05172             jobs=jobDOM.getElementsByTagName("job")
05173             for job in jobs:
05174                 jobcount+=1
05175                 write( "Processing Mythburn job number %s." % jobcount)
05176 
05177                 #get any options from the job file if present
05178                 options = job.getElementsByTagName("options")
05179                 if options.length > 0:
05180                     getOptions(options)
05181 
05182                 processJob(job)
05183 
05184             jobDOM.unlink()
05185 
05186             # clear the archiveitems table
05187             if clearArchiveTable == True:
05188                 clearArchiveItems()
05189 
05190             saveSetting("MythArchiveLastRunStatus", "Success")
05191             saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
05192             write("Finished processing jobs!!!")
05193         finally:
05194             # remove our lock file
05195             os.remove(lckpath)
05196 
05197             # make sure the files we created are read/writable by all 
05198             os.system("chmod -R a+rw-x+X %s" % defaultsettings["MythArchiveTempDir"])
05199     except SystemExit:
05200         write("Terminated")
05201     except:
05202         write('-'*60)
05203         traceback.print_exc(file=sys.stdout)
05204         if progresslog != "":
05205             traceback.print_exc(file=progressfile)
05206         write('-'*60)
05207         saveSetting("MythArchiveLastRunStatus", "Failed")
05208         saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
05209 
05210 if __name__ == "__main__":
05211     main()
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends