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 |
---|
34 | VERSION="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! |
---|
42 | debug_secondrunthrough = False |
---|
43 | |
---|
44 | # default encoding profile to use |
---|
45 | defaultEncodingProfile = "SP" |
---|
46 | |
---|
47 | #********************************************************************************* |
---|
48 | #Dont change the stuff below!! |
---|
49 | #********************************************************************************* |
---|
50 | import os, string, socket, sys, getopt, traceback, signal |
---|
51 | import xml.dom.minidom |
---|
52 | import Image, ImageDraw, ImageFont |
---|
53 | import MySQLdb, codecs |
---|
54 | import time, datetime, tempfile |
---|
55 | from fcntl import ioctl |
---|
56 | from CDROM import CDROMEJECT |
---|
57 | from CDROM import CDROMCLOSETRAY |
---|
58 | |
---|
59 | # media types (should match the enum in mytharchivewizard.h) |
---|
60 | DVD_SL = 0 |
---|
61 | DVD_DL = 1 |
---|
62 | DVD_RW = 2 |
---|
63 | FILE = 3 |
---|
64 | |
---|
65 | dvdPAL=(720,576) |
---|
66 | dvdNTSC=(720,480) |
---|
67 | dvdPALdpi=(75,80) |
---|
68 | dvdNTSCdpi=(81,72) |
---|
69 | |
---|
70 | dvdPALHalfD1="352x576" |
---|
71 | dvdNTSCHalfD1="352x480" |
---|
72 | dvdPALD1="%sx%s" % (dvdPAL[0],dvdPAL[1]) |
---|
73 | dvdNTSCD1="%sx%s" % (dvdNTSC[0],dvdNTSC[1]) |
---|
74 | |
---|
75 | #Single and dual layer recordable DVD free space in MBytes |
---|
76 | dvdrsize=(4482,8964) |
---|
77 | |
---|
78 | frameratePAL=25 |
---|
79 | framerateNTSC=29.97 |
---|
80 | |
---|
81 | #Just blank globals at startup |
---|
82 | temppath="" |
---|
83 | logpath="" |
---|
84 | scriptpath="" |
---|
85 | sharepath="" |
---|
86 | videopath="" |
---|
87 | recordingpath="" |
---|
88 | defaultsettings="" |
---|
89 | videomode="" |
---|
90 | gallerypath="" |
---|
91 | musicpath="" |
---|
92 | dateformat="" |
---|
93 | timeformat="" |
---|
94 | dbVersion="" |
---|
95 | preferredlang1="" |
---|
96 | preferredlang2="" |
---|
97 | useFIFO = True |
---|
98 | encodetoac3 = False |
---|
99 | alwaysRunMythtranscode = False |
---|
100 | copyremoteFiles = False |
---|
101 | |
---|
102 | #main menu aspect ratio (4:3 or 16:9) |
---|
103 | mainmenuAspectRatio = "16:9" |
---|
104 | |
---|
105 | #chapter menu aspect ratio (4:3, 16:9 or Video) |
---|
106 | #video means same aspect ratio as the video title |
---|
107 | chaptermenuAspectRatio = "Video" |
---|
108 | |
---|
109 | #default chapter length in seconds |
---|
110 | chapterLength = 5 * 60; |
---|
111 | |
---|
112 | #name of the default job file |
---|
113 | jobfile="mydata.xml" |
---|
114 | |
---|
115 | #progress log filename and file object |
---|
116 | progresslog = "" |
---|
117 | progressfile = open("/dev/null", 'w') |
---|
118 | |
---|
119 | #default location of DVD drive |
---|
120 | dvddrivepath = "/dev/dvd" |
---|
121 | |
---|
122 | #default option settings |
---|
123 | docreateiso = False |
---|
124 | doburn = True |
---|
125 | erasedvdrw = False |
---|
126 | mediatype = DVD_SL |
---|
127 | savefilename = '' |
---|
128 | |
---|
129 | configHostname = socket.gethostname() |
---|
130 | installPrefix = "" |
---|
131 | |
---|
132 | # job xml file |
---|
133 | jobDOM = None |
---|
134 | |
---|
135 | # theme xml file |
---|
136 | themeDOM = None |
---|
137 | themeName = '' |
---|
138 | |
---|
139 | #Maximum of 10 theme fonts |
---|
140 | themeFonts = [0,0,0,0,0,0,0,0,0,0] |
---|
141 | |
---|
142 | def 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 | |
---|
151 | def 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 | |
---|
159 | def getTempPath(): |
---|
160 | """This is the folder where all temporary files will be created.""" |
---|
161 | return temppath |
---|
162 | |
---|
163 | def getIntroPath(): |
---|
164 | """This is the folder where all intro files are located.""" |
---|
165 | return os.path.join(sharepath, "mytharchive", "intro") |
---|
166 | |
---|
167 | def getEncodingProfilePath(): |
---|
168 | """This is the folder where all encoder profile files are located.""" |
---|
169 | return os.path.join(sharepath, "mytharchive", "encoder_profiles") |
---|
170 | |
---|
171 | def 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 | |
---|
199 | def 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 | |
---|
203 | def doesFileExist(file): |
---|
204 | """Returns true/false if a given file or path exists.""" |
---|
205 | return os.path.exists( file ) |
---|
206 | |
---|
207 | def quoteFilename(filename): |
---|
208 | filename = filename.replace('"', '\\"') |
---|
209 | return '"%s"' % filename |
---|
210 | |
---|
211 | def 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 | |
---|
218 | def 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 | |
---|
232 | def getFontPathName(fontname): |
---|
233 | return os.path.join(sharepath, fontname) |
---|
234 | |
---|
235 | def getItemTempPath(itemnumber): |
---|
236 | return os.path.join(getTempPath(),"%s" % itemnumber) |
---|
237 | |
---|
238 | def 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 | |
---|
244 | def isResolutionHDTV(videoresolution): |
---|
245 | return (videoresolution[0]==1920 and videoresolution[1]==1080) or (videoresolution[0]==1280 and videoresolution[1]==720) |
---|
246 | |
---|
247 | def 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 | |
---|
253 | def getImageSize(sourcefile): |
---|
254 | myimage=Image.open(sourcefile,"r") |
---|
255 | return myimage.size |
---|
256 | |
---|
257 | def 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 | |
---|
263 | def 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 | |
---|
272 | def runCommand(command): |
---|
273 | checkCancelFlag() |
---|
274 | result=os.system(command) |
---|
275 | checkCancelFlag() |
---|
276 | return result |
---|
277 | |
---|
278 | def 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 | |
---|
310 | def 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 | |
---|
334 | def 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 | |
---|
344 | def 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 | |
---|
361 | def 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 | |
---|
377 | def 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 | |
---|
398 | def 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 | |
---|
415 | def 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 | |
---|
425 | def 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 | |
---|
451 | def 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 | |
---|
467 | def 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 | |
---|
527 | def 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 | |
---|
548 | def 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 | |
---|
601 | def 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 | |
---|
633 | def 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 | |
---|
641 | def 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 | |
---|
686 | def 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 | |
---|
711 | def 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 | |
---|
732 | def 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 | |
---|
746 | def 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 | |
---|
1025 | def 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 | |
---|
1032 | def 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 | |
---|
1068 | def 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 | |
---|
1083 | def 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 | |
---|
1124 | def 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 | |
---|
1134 | def 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 | |
---|
1155 | def 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 | |
---|
1179 | def 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 | |
---|
1208 | def 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 | |
---|
1219 | def 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 | |
---|
1297 | def 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 | |
---|
1386 | def 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 | |
---|
1394 | def 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 | |
---|
1406 | def 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 | |
---|
1442 | def 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 | |
---|
1486 | def 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 | |
---|
1498 | def 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 | |
---|
1528 | def 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 | |
---|
1573 | def 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 | |
---|
1891 | def 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 | |
---|
1902 | def 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 | |
---|
1992 | def 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 | |
---|
2212 | def 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 | |
---|
2337 | def 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 | |
---|
2460 | def 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 | |
---|
2536 | def isMediaAVIFile(file): |
---|
2537 | fh = open(file, 'rb') |
---|
2538 | Magic = fh.read(4) |
---|
2539 | fh.close() |
---|
2540 | return Magic=="RIFF" |
---|
2541 | |
---|
2542 | def 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 |
---|
2579 | VIDEO_INDEX = 0 |
---|
2580 | VIDEO_CODEC = 1 |
---|
2581 | VIDEO_ID = 2 |
---|
2582 | |
---|
2583 | AUDIO_INDEX = 0 |
---|
2584 | AUDIO_CODEC = 1 |
---|
2585 | AUDIO_ID = 2 |
---|
2586 | AUDIO_LANG = 3 |
---|
2587 | |
---|
2588 | def 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 | |
---|
2731 | def 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 | |
---|
2769 | def 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 | |
---|
2788 | def 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 | |
---|
2808 | def 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 | |
---|
2837 | def 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 | |
---|
3026 | def 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 | |
---|
3077 | def 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 | |
---|
3256 | def 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 | |
---|
3270 | write( "mythburn.py (%s) starting up..." % VERSION) |
---|
3271 | |
---|
3272 | nice=os.nice(8) |
---|
3273 | write( "Process priority %s" % nice) |
---|
3274 | |
---|
3275 | #Ensure were running at least python 2.3.5 |
---|
3276 | if 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 |
---|
3281 | scriptpath = os.path.dirname(sys.argv[0]) |
---|
3282 | scriptpath = os.path.abspath(scriptpath) |
---|
3283 | write("script path:" + scriptpath) |
---|
3284 | |
---|
3285 | # figure out where the myth share directory is located |
---|
3286 | sharepath = os.path.split(scriptpath)[0] |
---|
3287 | sharepath = os.path.split(sharepath)[0] |
---|
3288 | write("myth share path:" + sharepath) |
---|
3289 | |
---|
3290 | # process any command line options |
---|
3291 | try: |
---|
3292 | opts, args = getopt.getopt(sys.argv[1:], "j:hl:", ["jobfile=", "help", "progresslog="]) |
---|
3293 | except getopt.GetoptError: |
---|
3294 | # print usage and exit |
---|
3295 | usage() |
---|
3296 | sys.exit(2) |
---|
3297 | |
---|
3298 | for 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 |
---|
3310 | if 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 |
---|
3318 | getMysqlDBParameters(); |
---|
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 |
---|
3323 | if not os.environ['PATH'].endswith(':'): |
---|
3324 | os.environ['PATH'] += ":" |
---|
3325 | os.environ['PATH'] += "/bin:/sbin:/usr/local/bin:/usr/bin:/opt/bin:" + installPrefix +"/bin:" |
---|
3326 | |
---|
3327 | #Get defaults from MythTV database |
---|
3328 | defaultsettings = getDefaultParametersFromMythTVDB() |
---|
3329 | videopath = defaultsettings.get("VideoStartupDir", None) |
---|
3330 | recordingpath = defaultsettings.get("RecordFilePrefix", None) |
---|
3331 | gallerypath = defaultsettings.get("GalleryDir", None) |
---|
3332 | musicpath = defaultsettings.get("MusicLocation", None) |
---|
3333 | videomode = string.lower(defaultsettings["MythArchiveVideoFormat"]) |
---|
3334 | temppath = defaultsettings["MythArchiveTempDir"] + "/work" |
---|
3335 | logpath = defaultsettings["MythArchiveTempDir"] + "/logs" |
---|
3336 | dvddrivepath = defaultsettings["MythArchiveDVDLocation"] |
---|
3337 | dbVersion = defaultsettings["DBSchemaVer"] |
---|
3338 | preferredlang1 = defaultsettings["ISO639Language0"] |
---|
3339 | preferredlang2 = defaultsettings["ISO639Language1"] |
---|
3340 | useFIFO = (defaultsettings["MythArchiveUseFIFO"] == '1') |
---|
3341 | encodetoac3 = (defaultsettings["MythArchiveEncodeToAc3"] == '1') |
---|
3342 | alwaysRunMythtranscode = (defaultsettings["MythArchiveAlwaysUseMythTranscode"] == '1') |
---|
3343 | copyremoteFiles = (defaultsettings["MythArchiveCopyRemoteFiles"] == '1') |
---|
3344 | mainmenuAspectRatio = defaultsettings["MythArchiveMainMenuAR"] |
---|
3345 | chaptermenuAspectRatio = defaultsettings["MythArchiveChapterMenuAR"] |
---|
3346 | |
---|
3347 | # external commands |
---|
3348 | path_mplex = [defaultsettings["MythArchiveMplexCmd"], os.path.split(defaultsettings["MythArchiveMplexCmd"])[1]] |
---|
3349 | path_ffmpeg = [defaultsettings["MythArchiveFfmpegCmd"], os.path.split(defaultsettings["MythArchiveFfmpegCmd"])[1]] |
---|
3350 | path_dvdauthor = [defaultsettings["MythArchiveDvdauthorCmd"], os.path.split(defaultsettings["MythArchiveDvdauthorCmd"])[1]] |
---|
3351 | path_mkisofs = [defaultsettings["MythArchiveMkisofsCmd"], os.path.split(defaultsettings["MythArchiveMkisofsCmd"])[1]] |
---|
3352 | path_growisofs = [defaultsettings["MythArchiveGrowisofsCmd"], os.path.split(defaultsettings["MythArchiveGrowisofsCmd"])[1]] |
---|
3353 | path_tcrequant = [defaultsettings["MythArchiveTcrequantCmd"], os.path.split(defaultsettings["MythArchiveTcrequantCmd"])[1]] |
---|
3354 | path_png2yuv = [defaultsettings["MythArchivePng2yuvCmd"], os.path.split(defaultsettings["MythArchivePng2yuvCmd"])[1]] |
---|
3355 | path_spumux = [defaultsettings["MythArchiveSpumuxCmd"], os.path.split(defaultsettings["MythArchiveSpumuxCmd"])[1]] |
---|
3356 | path_mpeg2enc = [defaultsettings["MythArchiveMpeg2encCmd"], os.path.split(defaultsettings["MythArchiveMpeg2encCmd"])[1]] |
---|
3357 | |
---|
3358 | try: |
---|
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"]) |
---|
3405 | except SystemExit: |
---|
3406 | write("Terminated") |
---|
3407 | except: |
---|
3408 | write('-'*60) |
---|
3409 | traceback.print_exc(file=sys.stdout) |
---|
3410 | if progresslog != "": |
---|
3411 | traceback.print_exc(file=progressfile) |
---|
3412 | write('-'*60) |
---|