Ticket #3282: mythburn.py

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

My new and improved mythburn.py script

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