|
MythTV
0.26-pre
|
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()
1.7.6.1