MythTV master
mythburn.py
Go to the documentation of this file.
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# python 3 doesn't have a unicode type
5try:
6 unicode
7except:
8 unicode = str
9
10# python 3 doesn't have a long type
11try:
12 long
13except:
14 long = int
15
16
17# mythburn.py
18# The ported MythBurn scripts which feature:
19
20# Burning of recordings (including HDTV) and videos
21# of ANY format to DVDR. Menus are created using themes
22# and are easily customised.
23
24# See mydata.xml for format of input file
25
26# spit2k1
27# 11 January 2006
28# 6 Feb 2006 - Added into CVS for the first time
29
30# paulh
31# 4 May 2006 - Added into mythtv svn
32
33# For this script to work you need to have...
34# Python - v2.6 or later
35# mythtv python bindings installed
36# python-imaging (PIL) or Pillow the fork of PIL
37# dvdauthor - v0.6.14
38# dvd+rw-tools - v7.1
39# cdrtools - v3.01
40
41# Optional for shrink-to-fit requantisation
42# M2VRequantiser (from flexion, based on the newer code from Metakine)
43
44# Optional (for Right To Left languages)
45# pyfribidi
46
47# Optional (alternate demuxer)
48# ProjectX - >=0.91
49
50# Optional (to lower the ionice level)
51# psutil
52
53#******************************************************************************
54#******************************************************************************
55#******************************************************************************
56
57
58# All strings in this file should be unicode, not byte string!! They get converted to utf-8 only
59
60
61
62
63# version of script - change after each update
64VERSION="0.2.20200122-1"
65
66# keep all temporary files for debugging purposes
67# set this to True before a first run through when testing
68# out new themes (see below)
69debug_keeptempfiles = False
70
71
77debug_secondrunthrough = False
78
79# default encoding profile to use
80defaultEncodingProfile = "SP"
81
82# add audio sync offset when re-muxing
83useSyncOffset = True
84
85# if the theme doesn't have a chapter menu and this is set to true then the
86# chapter marks will be set to the cut point end marks
87addCutlistChapters = False
88
89# change this to True to always convert any audio tracks to ac3 for better compatibility
90encodetoac3 = False
91
92#*********************************************************************************
93#Dont change the stuff below!!
94#*********************************************************************************
95import os
96import sys
97import string
98import getopt
99import traceback
100import signal
101import xml.dom.minidom
102from PIL import Image
103from PIL import ImageDraw
104from PIL import ImageFont
105from PIL import ImageColor
106import unicodedata
107import time
108import tempfile
109from fcntl import ioctl
110
111try:
112 import CDROM
113except:
114 # Some hardcoded values for ioctl calls,
115 # not available on python > 3.5, see include/linux/cdrom.h
116 class CDROM(object):
117 CDS_NO_INFO = 0
118 CDS_NO_DISC = 1
119 CDS_TRAY_OPEN = 2
120 CDS_DRIVE_NOT_READY = 3
121 CDS_DISC_OK = 4
122 CDROMEJECT = 0x5309
123 CDROMRESET = 0x5312
124 CDROM_DRIVE_STATUS = 0x5326
125 CDROM_LOCKDOOR = 0x5329
126
127from shutil import copy
128
129import MythTV
130from MythTV import datetime
131from MythTV.altdict import OrdDict
132
133# media types (should match the enum in mytharchivewizard.h)
134DVD_SL = 0
135DVD_DL = 1
136DVD_RW = 2
137FILE = 3
138
139dvdPAL=(720,576)
140dvdNTSC=(720,480)
141dvdPALdpi=(75,80)
142dvdNTSCdpi=(81,72)
143
144dvdPALHalfD1="352x576"
145dvdNTSCHalfD1="352x480"
146dvdPALD1="%sx%s" % (dvdPAL[0],dvdPAL[1])
147dvdNTSCD1="%sx%s" % (dvdNTSC[0],dvdNTSC[1])
148
149#Single and dual layer recordable DVD free space in MBytes
150dvdrsize=(4482,8106)
151
152frameratePAL=25
153framerateNTSC=29.97
154
155#any aspect ratio above this value is assumed to be 16:9
156aspectRatioThreshold = 1.4
157
158#Just blank globals at startup
159temppath=""
160logpath=""
161scriptpath=""
162sharepath=""
163videopath=""
164defaultsettings=""
165videomode=""
166gallerypath=""
167musicpath=""
168dateformat=""
169timeformat=""
170dbVersion=""
171preferredlang1=""
172preferredlang2=""
173useFIFO = True
174alwaysRunMythtranscode = False
175copyremoteFiles = False
176thumboffset = 10
177usebookmark = True
178clearArchiveTable = True
179nicelevel = 17;
180drivespeed = 0;
181
182#main menu aspect ratio (4:3 or 16:9)
183mainmenuAspectRatio = "16:9"
184
185#chapter menu aspect ratio (4:3, 16:9 or Video)
186#video means same aspect ratio as the video title
187chaptermenuAspectRatio = "Video"
188
189#default chapter length in seconds
190chapterLength = 5 * 60;
191
192#name of the default job file
193jobfile="mydata.xml"
194
195#progress log filename and file object
196progresslog = ""
197progressfile = open("/dev/null", 'w', encoding='utf-8')
198
199#default location of DVD drive
200dvddrivepath = "/dev/dvd"
201
202#default option settings
203docreateiso = False
204doburn = True
205erasedvdrw = False
206mediatype = DVD_SL
207savefilename = ''
208
209installPrefix = ""
210
211# job xml file
212jobDOM = None
213
214# theme xml file
215themeDOM = None
216themeName = ''
217
218#dictionary of font definitions used in theme
219themeFonts = {}
220
221# no. of processors we have access to
222cpuCount = 1
223
224DB = MythTV.MythDB()
225MVID = MythTV.MythVideo(db=DB)
226Video = MythTV.Video
227
228configHostname = DB.gethostname()
229
230
231
232# mytharchivehelper needes this locale to work correctly
233try:
234 oldlocale = os.environ["LC_ALL"]
235except:
236 oldlocale = ""
237os.putenv("LC_ALL", "en_US.UTF-8")
238
239
240# fix rtl text where pyfribidi is not available
241# should write a simple algorithm, meanwhile just return the original string
243 return str
244
245# Bind the name fix_rtl to the appropriate function
246try:
247 import pyfribidi
248except ImportError:
249 sys.stdout.write("Using simple_fix_rtl\n")
250 fix_rtl = simple_fix_rtl
251else:
252 sys.stdout.write("Using pyfribidi.log2vis\n")
253 fix_rtl = pyfribidi.log2vis
254
255
257
258class FontDef(object):
259 def __init__(self, name=None, fontFile=None, size=19, color="white", effect="normal", shadowColor="black", shadowSize=1):
260 self.name = name
261 self.fontFile = fontFile
262 self.size = size
263 self.color = color
264 self.effect = effect
265 self.shadowColor = shadowColor
266 self.shadowSize = shadowSize
267 self.font = None
268
269 def getFont(self):
270 if self.font is None:
271 self.font = ImageFont.truetype(self.fontFile, int(self.size))
272
273 return self.font
274
275 def drawText(self, text, color=None):
276 if self.font is None:
277 self.font = ImageFont.truetype(self.fontFile, int(self.size))
278
279 if color is None:
280 color = self.color
281
282 textwidth, textheight = self.font.getsize(text)
283
284 image = Image.new("RGBA", (textwidth + (self.shadowSize * 2), textheight), (0,0,0,0))
285 draw = ImageDraw.ImageDraw(image)
286
287 if self.effect == "shadow":
288 draw.text((self.shadowSize,self.shadowSize), text, font=self.font, fill=self.shadowColor)
289 draw.text((0,0), text, font=self.font, fill=color)
290 elif self.effect == "outline":
291 for x in range(0, self.shadowSize * 2 + 1):
292 for y in range(0, self.shadowSize * 2 + 1):
293 draw.text((x, y), text, font=self.font, fill=self.shadowColor)
294
295 draw.text((self.shadowSize,self.shadowSize), text, font=self.font, fill=color)
296 else:
297 draw.text((0,0), text, font=self.font, fill=color)
298
299 bbox = image.getbbox()
300 image = image.crop(bbox)
301 return image
302
303
305
306def write(text, progress=True):
307 """Simple place to channel all text output through"""
308
309 if sys.version_info == 2:
310 sys.stdout.write((text + "\n").encode("utf-8", "replace"))
311 else:
312 sys.stdout.write(text + "\n")
313 sys.stdout.flush()
314
315 if progress == True and progresslog != "":
316 progressfile.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n")
317 progressfile.flush()
318
319
321
322def fatalError(msg):
323 """Display an error message and exit app"""
324 write("*"*60)
325 write("ERROR: " + msg)
326 write("See mythburn.log for more information.")
327 write("*"*60)
328 write("")
329 saveSetting("MythArchiveLastRunResult", "Failed: " + quoteString(msg));
330 saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
331 sys.exit(0)
332
333# ###########################################################
334# Display a warning message
335
337 """Display a warning message"""
338 write("*"*60)
339 write("WARNING: " + msg)
340 write("*"*60)
341 write("")
342
343
345
346def quoteString(str):
347 """Return the input string with single quotes escaped."""
348 return str.replace("'", "'\"'\"'")
349
350
352
354 """This is the folder where all temporary files will be created."""
355 return temppath
356
357
359
361 """return the number of CPUs"""
362 # /proc/cpuinfo
363 cpustat = open("/proc/cpuinfo", 'r', encoding='utf-8')
364 cpudata = cpustat.readlines()
365 cpustat.close()
366
367 cpucount = 0
368 for line in cpudata:
369 tokens = line.split()
370 if len(tokens) > 0:
371 if tokens[0] == "processor":
372 cpucount += 1
373
374 if cpucount == 0:
375 cpucount = 1
376
377 write("Found %d CPUs" % cpucount)
378
379 return cpucount
380
381
383
385 """This is the folder where all encoder profile files are located."""
386 return os.path.join(sharepath, "mytharchive", "encoder_profiles")
387
388
390
392 """Returns true/false if a given file or path exists."""
393 return os.path.exists( file )
394
395
397
398def quoteCmdArg(arg):
399 arg = arg.replace('"', '\\"')
400 arg = arg.replace('`', '\\`')
401 return '"%s"' % arg
402
403
405
406def getText(node):
407 """Returns the text contents from a given XML element."""
408 if node.childNodes.length>0:
409 return node.childNodes[0].data
410 else:
411 return ""
412
413
415
416def getThemeFile(theme,file):
417 """Find a theme file - first look in the specified theme directory then look in the
418 shared music and image directories"""
419 if os.path.exists(os.path.join(sharepath, "mytharchive", "themes", theme, file)):
420 return os.path.join(sharepath, "mytharchive", "themes", theme, file)
421
422 if os.path.exists(os.path.join(sharepath, "mytharchive", "images", file)):
423 return os.path.join(sharepath, "mytharchive", "images", file)
424
425 if os.path.exists(os.path.join(sharepath, "mytharchive", "intro", file)):
426 return os.path.join(sharepath, "mytharchive", "intro", file)
427
428 if os.path.exists(os.path.join(sharepath, "mytharchive", "music", file)):
429 return os.path.join(sharepath, "mytharchive", "music", file)
430
431 fatalError("Cannot find theme file '%s' in theme '%s'" % (file, theme))
432
433
435
436def getFontPathName(fontname):
437 return os.path.join(sharepath, "fonts", fontname)
438
439
441
442def getItemTempPath(itemnumber):
443 return os.path.join(getTempPath(),"%s" % itemnumber)
444
445
447
448def validateTheme(theme):
449 #write( "Checking theme", theme
450 file = getThemeFile(theme,"theme.xml")
451 write("Looking for: " + file)
452 return doesFileExist( getThemeFile(theme,"theme.xml") )
453
454
456
457def isResolutionOkayForDVD(videoresolution):
458 if videomode=="ntsc":
459 return videoresolution==(720,480) or videoresolution==(704,480) or videoresolution==(352,480) or videoresolution==(352,240)
460 else:
461 return videoresolution==(720,576) or videoresolution==(704,576) or videoresolution==(352,576) or videoresolution==(352,288)
462
463
465
467 """Does what it says on the tin!."""
468 for root, dirs, deletefiles in os.walk(folder, topdown=False):
469 for name in deletefiles:
470 os.remove(os.path.join(root, name))
471
472
474
476 for root, dirs, files in os.walk(folder, topdown=False):
477 for name in files:
478 os.remove(os.path.join(root, name))
479 for name in dirs:
480 if os.path.islink(os.path.join(root, name)):
481 os.remove(os.path.join(root, name))
482 else:
483 os.rmdir(os.path.join(root, name))
484
485
487
489 """Checks to see if the user has cancelled this run"""
490 if os.path.exists(os.path.join(logpath, "mythburncancel.lck")):
491 os.remove(os.path.join(logpath, "mythburncancel.lck"))
492 write('*'*60)
493 write("Job has been cancelled at users request")
494 write('*'*60)
495 sys.exit(1)
496
497
500
501def runCommand(command):
503
504 result = os.system(command.encode('utf-8'))
505
506 if os.WIFEXITED(result):
507 result = os.WEXITSTATUS(result)
509 return result
510
511
513
514def secondsToFrames(seconds):
515 """Convert a time in seconds to a frame position"""
516 if videomode=="pal":
517 framespersecond=frameratePAL
518 else:
519 framespersecond=framerateNTSC
520
521 frames=int(seconds * framespersecond)
522 return frames
523
524
526
527def encodeMenu(background, tempvideo, music, musiclength, tempmovie, xmlfile, finaloutput, aspectratio):
528 if videomode=="pal":
529 framespersecond=frameratePAL
530 else:
531 framespersecond=framerateNTSC
532
533 totalframes=int(musiclength * framespersecond)
534
535 command = quoteCmdArg(path_jpeg2yuv[0]) + " -n %s -v0 -I p -f %s -j %s | %s -b 5000 -a %s -v 1 -f 8 -o %s" \
536 % (totalframes, framespersecond, quoteCmdArg(background), quoteCmdArg(path_mpeg2enc[0]), aspectratio, quoteCmdArg(tempvideo))
537 result = runCommand(command)
538 if result!=0:
539 fatalError("Failed while running jpeg2yuv - %s" % command)
540
541 command = quoteCmdArg(path_mplex[0]) + " -f 8 -v 0 -o %s %s %s" % (quoteCmdArg(tempmovie), quoteCmdArg(tempvideo), quoteCmdArg(music))
542 result = runCommand(command)
543 if result!=0:
544 fatalError("Failed while running mplex - %s" % command)
545
546 if xmlfile != "":
547 command = quoteCmdArg(path_spumux[0]) + " -m dvd -s 0 %s < %s > %s" % (quoteCmdArg(xmlfile), quoteCmdArg(tempmovie), quoteCmdArg(finaloutput))
548 result = runCommand(command)
549 if result!=0:
550 fatalError("Failed while running spumux - %s" % command)
551 else:
552 os.rename(tempmovie, finaloutput)
553
554 if os.path.exists(tempvideo):
555 os.remove(tempvideo)
556 if os.path.exists(tempmovie):
557 os.remove(tempmovie)
558
559
562
564 """Returns the XML node for the given encoding profile"""
565
566 # which encoding file do we need
567
568 # first look for a custom profile file in ~/.mythtv/MythArchive/
569 if videomode == "ntsc":
570 filename = os.path.expanduser("~/.mythtv/MythArchive/ffmpeg_dvd_ntsc.xml")
571 else:
572 filename = os.path.expanduser("~/.mythtv/MythArchive/ffmpeg_dvd_pal.xml")
573
574 if not os.path.exists(filename):
575 # not found so use the default profiles
576 if videomode == "ntsc":
577 filename = getEncodingProfilePath() + "/ffmpeg_dvd_ntsc.xml"
578 else:
579 filename = getEncodingProfilePath() + "/ffmpeg_dvd_pal.xml"
580
581 write("Using encoder profiles from %s" % filename)
582
583 DOM = xml.dom.minidom.parse(filename)
584
585 #Error out if its the wrong XML
586 if DOM.documentElement.tagName != "encoderprofiles":
587 fatalError("Profile xml file doesn't look right (%s)" % filename)
588
589 profiles = DOM.getElementsByTagName("profile")
590 for node in profiles:
591 if getText(node.getElementsByTagName("name")[0]) == profile:
592 write("Encoding profile (%s) found" % profile)
593 return node
594
595 write('WARNING: Encoding profile "' + profile + '" not found.')
596 write('Using default profile "' + defaultEncodingProfile + '".')
597 for node in profiles:
598 if getText(node.getElementsByTagName("name")[0]) == defaultEncodingProfile:
599 write("Encoding profile (%s) found" % defaultEncodingProfile)
600 return node
601
602 fatalError('Neither encoding profile "' + profile + '" nor default enocding profile "' + defaultEncodingProfile + '" found. Giving up.')
603 return None
604
605
607
609 """Loads the XML file from disk for a specific theme"""
610
611 #Load XML input file from disk
612 themeDOM = xml.dom.minidom.parse( getThemeFile(theme,"theme.xml") )
613 #Error out if its the wrong XML
614 if themeDOM.documentElement.tagName != "mythburntheme":
615 fatalError("Theme xml file doesn't look right (%s)" % theme)
616 return themeDOM
617
618
620
622 """Returns the length of a video file (in seconds)"""
623
624 #open the XML containing information about this file
625 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
626
627 #error out if its the wrong XML
628 if infoDOM.documentElement.tagName != "file":
629 fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
630 file = infoDOM.getElementsByTagName("file")[0]
631 if file.attributes["cutduration"].value != 'N/A':
632 duration = int(file.attributes["cutduration"].value)
633 else:
634 duration = 0;
635
636 return duration
637
638
641
642def getAudioParams(folder):
643 """Returns the audio bitrate and no of channels for a file from its streaminfo.xml"""
644
645 #open the XML containing information about this file
646 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
647
648 #error out if its the wrong XML
649 if infoDOM.documentElement.tagName != "file":
650 fatalError("Stream info file doesn't look right (%s)" % os.path.join(folder, 'streaminfo.xml'))
651 audio = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("audio")[0]
652
653 samplerate = audio.attributes["samplerate"].value
654 channels = audio.attributes["channels"].value
655
656 return (samplerate, channels)
657
658
661
662def getVideoParams(folder):
663 """Returns the video resolution, fps and aspect ratio for the video file from the streaminfo.xml file"""
664
665 #open the XML containing information about this file
666 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
667
668 #error out if its the wrong XML
669 if infoDOM.documentElement.tagName != "file":
670 fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
671 video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
672
673 if video.attributes["aspectratio"].value != 'N/A':
674 aspect_ratio = video.attributes["aspectratio"].value
675 else:
676 aspect_ratio = "1.77778"
677
678 videores = video.attributes["width"].value + 'x' + video.attributes["height"].value
679 fps = video.attributes["fps"].value
680
681 #sanity check the fps
682 if videomode=="pal":
683 fr=frameratePAL
684 else:
685 fr=framerateNTSC
686
687 if float(fr) != float(fps):
688 write("WARNING: frames rates do not match")
689 write("The frame rate for %s should be %s but the stream info file "
690 "report a fps of %s" % (videomode, fr, fps))
691 fps = fr
692
693 return (videores, fps, aspect_ratio)
694
695
697
699 """Returns the aspect ratio of the video file (1.333, 1.778, etc)"""
700
701 #open the XML containing information about this file
702 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
703
704 #error out if its the wrong XML
705 if infoDOM.documentElement.tagName != "file":
706 fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo.xml'))
707 video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
708 if video.attributes["aspectratio"].value != 'N/A':
709 aspect_ratio = float(video.attributes["aspectratio"].value)
710 else:
711 aspect_ratio = 1.77778; # default
712 write("aspect ratio is: %s" % aspect_ratio)
713 return aspect_ratio
714
715
717
718def calcSyncOffset(index):
719 """Returns the sync offset between the video and first audio stream"""
720
721 #open the XML containing information about this file
722 #infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo_orig.xml'))
723 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(index), 'streaminfo.xml'))
724
725 #error out if its the wrong XML
726 if infoDOM.documentElement.tagName != "file":
727 fatalError("Stream info file doesn't look right (%s)" % os.path.join(getItemTempPath(index), 'streaminfo_orig.xml'))
728
729 video = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("video")[0]
730 video_start = float(video.attributes["start_time"].value)
731
732 audio = infoDOM.getElementsByTagName("file")[0].getElementsByTagName("streams")[0].getElementsByTagName("audio")[0]
733 audio_start = float(audio.attributes["start_time"].value)
734
735# write("Video start time is: %s" % video_start)
736# write("Audio start time is: %s" % audio_start)
737
738 sync_offset = int((video_start - audio_start) * 1000)
739
740# write("Sync offset is: %s" % sync_offset)
741 return sync_offset
742
743
745
747 duration = getLengthOfVideo(index)
748
749 minutes = int(duration / 60)
750 seconds = duration % 60
751 hours = int(minutes / 60)
752 minutes %= 60
753
754 return '%02d:%02d:%02d' % (hours, minutes, seconds)
755
756
758
759def frameToTime(frame, fps):
760 sec = int(frame / fps)
761 frame = frame - int(sec * fps)
762 mins = sec // 60
763 sec %= 60
764 hour = mins // 60
765 mins %= 60
766
767 return '%02d:%02d:%02d' % (hour, mins, sec)
768
769
771
772def timeStringToSeconds(formatedtime):
773 parts = formatedtime.split(':')
774 if len(parts) != 3:
775 return 0
776
777 sec = int(parts[2])
778 mins = int(parts[1])
779 hour = int(parts[0])
780
781 return sec + (mins * 60) + (hour * 60 * 60)
782
783
786
787def createVideoChapters(itemnum, numofchapters, lengthofvideo, getthumbnails):
788 """Returns numofchapters chapter marks even spaced through a certain time period"""
789
790 # if there are user defined thumb images already available use them
791 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum),"info.xml"))
792 thumblistNode = infoDOM.getElementsByTagName("thumblist")
793 if thumblistNode.length > 0:
794 thumblist = getText(thumblistNode[0])
795 write("Using user defined thumb images - %s" % thumblist)
796 return thumblist
797
798 # no user defined thumbs so create them
799 segment=int(lengthofvideo / numofchapters)
800
801 write( "Video length is %s seconds. Each chapter will be %s seconds" % (lengthofvideo,segment))
802
803 chapters=[]
804
805 thumbList=[]
806 starttime=0
807 count=1
808 while count<=numofchapters:
809 chapters.append(time.strftime("%H:%M:%S",time.gmtime(starttime)))
810
811 if starttime==0:
812 if thumboffset < segment:
813 thumbList.append(str(thumboffset))
814 else:
815 thumbList.append(str(starttime))
816 else:
817 thumbList.append(str(starttime))
818
819 starttime+=segment
820 count+=1
821
822 chapters = ','.join(chapters)
823 thumbList = ','.join(thumbList)
824
825 if getthumbnails==True:
826 extractVideoFrames( os.path.join(getItemTempPath(itemnum),"stream.mv2"),
827 os.path.join(getItemTempPath(itemnum),"chapter-%1.jpg"), thumbList)
828
829 return chapters
830
831
833
834def createVideoChaptersFixedLength(itemnum, segment, lengthofvideo):
835 """Returns chapter marks at cut list ends,
836 or evenly spaced chapters 'segment' seconds through the file"""
837
838
839 if addCutlistChapters == True:
840 # we've been asked to use the cut list as chapter marks
841 # so if there is a cut list available, use it
842
843 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum),"info.xml"))
844 chapterlistNode = infoDOM.getElementsByTagName("chapterlist")
845 if chapterlistNode.length > 0:
846 chapterlist = getText(chapterlistNode[0])
847 write("Using commercial end marks - %s" % chapterlist)
848 return chapterlist
849
850 if lengthofvideo < segment:
851 return "00:00:00"
852
853 numofchapters = lengthofvideo // segment + 1;
854 chapters = "00:00:00"
855 starttime = 0
856 count = 2
857 while count <= numofchapters:
858 starttime += segment
859 chapters += "," + time.strftime("%H:%M:%S", time.gmtime(starttime))
860 count += 1
861
862 write("Fixed length chapters: %s" % chapters)
863
864 return chapters
865
866
868
870 """Reads settings from MythTV database"""
871
872 write( "Obtaining MythTV settings from MySQL database for hostname " + configHostname)
873
874 #DBSchemaVer, ISO639Language0 ISO639Language1 are not dependant upon the hostname.
875 sqlstatement="""SELECT value, data FROM settings WHERE value IN(
876 'DBSchemaVer',
877 'ISO639Language0',
878 'ISO639Language1')
879 OR (hostname=%s AND value IN(
880 'VideoStartupDir',
881 'GalleryDir',
882 'MusicLocation',
883 'MythArchiveVideoFormat',
884 'MythArchiveTempDir',
885 'MythArchiveMplexCmd',
886 'MythArchiveDvdauthorCmd',
887 'MythArchiveMkisofsCmd',
888 'MythArchiveM2VRequantiserCmd',
889 'MythArchiveMpg123Cmd',
890 'MythArchiveProjectXCmd',
891 'MythArchiveDVDLocation',
892 'MythArchiveGrowisofsCmd',
893 'MythArchiveJpeg2yuvCmd',
894 'MythArchiveSpumuxCmd',
895 'MythArchiveMpeg2encCmd',
896 'MythArchiveCopyRemoteFiles',
897 'MythArchiveAlwaysUseMythTranscode',
898 'MythArchiveUseProjectX',
899 'MythArchiveAddSubtitles',
900 'MythArchiveUseFIFO',
901 'MythArchiveMainMenuAR',
902 'MythArchiveChapterMenuAR',
903 'MythArchiveDateFormat',
904 'MythArchiveTimeFormat',
905 'MythArchiveClearArchiveTable',
906 'MythArchiveDriveSpeed',
907 'JobQueueCPU'
908 )) ORDER BY value"""
909
910 # create a cursor
911 cursor = DB.cursor()
912 # execute SQL statement
913 cursor.execute(sqlstatement, (configHostname,))
914 # get the resultset as a tuple
915 result = cursor.fetchall()
916
917 cfg = {}
918 for i in range(len(result)):
919 cfg[result[i][0]] = result[i][1]
920
921 #bail out if we can't find the temp dir setting
922 if not "MythArchiveTempDir" in cfg:
923 fatalError("Can't find the setting for the temp directory. \nHave you run setup in the frontend?")
924 return cfg
925
926
928
929def saveSetting(name, data):
930 host = DB.gethostname()
931 DB.settings[host][name] = data
932
933
935
937 ''' Remove all archive items from the archiveitems DB table'''
938
939 write("Removing all archive items from the archiveitems DB table")
940 with DB as cursor:
941 cursor.execute("DELETE FROM archiveitems")
942
943
945
946def getOptions(options):
947 global doburn
948 global docreateiso
949 global erasedvdrw
950 global mediatype
951 global savefilename
952
953 if options.length == 0:
954 fatalError("Trying to read the options from the job file but none found?")
955 options = options[0]
956
957 doburn = options.attributes["doburn"].value != '0'
958 docreateiso = options.attributes["createiso"].value != '0'
959 erasedvdrw = options.attributes["erasedvdrw"].value != '0'
960 mediatype = int(options.attributes["mediatype"].value)
961 savefilename = options.attributes["savefilename"].value
962
963 write("Options - mediatype = %d, doburn = %d, createiso = %d, erasedvdrw = %d" \
964 % (mediatype, doburn, docreateiso, erasedvdrw))
965 write(" savefilename = '%s'" % savefilename)
966
967
969
970def expandItemText(infoDOM, text, itemnumber, pagenumber, keynumber,chapternumber, chapterlist ):
971 """Replaces keywords in a string with variables from the XML and filesystem"""
972 text=text.replace("%page","%s" % pagenumber)
973
974 #See if we can use the thumbnail/cover file for videos if there is one.
975 if getText( infoDOM.getElementsByTagName("coverfile")[0]) =="":
976 text=text.replace("%thumbnail", os.path.join( getItemTempPath(itemnumber), "title.jpg"))
977 else:
978 text=text.replace("%thumbnail", getText( infoDOM.getElementsByTagName("coverfile")[0]) )
979
980 text=text.replace("%itemnumber","%s" % itemnumber )
981 text=text.replace("%keynumber","%s" % keynumber )
982
983 text=text.replace("%title",getText( infoDOM.getElementsByTagName("title")[0]) )
984 text=text.replace("%subtitle",getText( infoDOM.getElementsByTagName("subtitle")[0]) )
985 text=text.replace("%description",getText( infoDOM.getElementsByTagName("description")[0]) )
986 text=text.replace("%type",getText( infoDOM.getElementsByTagName("type")[0]) )
987
988 text=text.replace("%recordingdate",getText( infoDOM.getElementsByTagName("recordingdate")[0]) )
989 text=text.replace("%recordingtime",getText( infoDOM.getElementsByTagName("recordingtime")[0]) )
990
991 text=text.replace("%duration", getFormatedLengthOfVideo(itemnumber))
992
993 text=text.replace("%myfolder",getThemeFile(themeName,""))
994
995 if chapternumber>0:
996 text=text.replace("%chapternumber","%s" % chapternumber )
997 text=text.replace("%chaptertime","%s" % chapterlist[chapternumber - 1] )
998 text=text.replace("%chapterthumbnail", os.path.join( getItemTempPath(itemnumber), "chapter-%s.jpg" % chapternumber))
999
1000 return text
1001
1002
1004
1005def getScaledAttribute(node, attribute):
1006 """ Returns a value taken from attribute in node scaled for the current video mode"""
1007
1008 if videomode == "pal" or attribute == "x" or attribute == "w":
1009 return int(node.attributes[attribute].value)
1010 else:
1011 return int(float(node.attributes[attribute].value) / 1.2)
1012
1013
1015
1016def intelliDraw(drawer, text, font, containerWidth):
1017 """Based on http://mail.python.org/pipermail/image-sig/2004-December/003064.html"""
1018 #Args:
1019 # drawer: Instance of "ImageDraw.Draw()"
1020 # text: string of long text to be wrapped
1021 # font: instance of ImageFont (I use .truetype)
1022 # containerWidth: number of pixels text lines have to fit into.
1023
1024 #write("containerWidth: %s" % containerWidth)
1025 words = text.split()
1026 lines = [] # prepare a return argument
1027 lines.append(words)
1028 finished = False
1029 line = 0
1030 while not finished:
1031 thistext = lines[line]
1032 newline = []
1033 innerFinished = False
1034 while not innerFinished:
1035 #write( 'thistext: '+str(thistext))
1036 #write("textWidth: %s" % drawer.textsize(' '.join(thistext),font)[0])
1037
1038 if drawer.textsize(' '.join(thistext),font.getFont())[0] > containerWidth:
1039 # this is the heart of the algorithm: we pop words off the current
1040 # sentence until the width is ok, then in the next outer loop
1041 # we move on to the next sentence.
1042 if str(thistext).find(' ') != -1:
1043 newline.insert(0,thistext.pop(-1))
1044 else:
1045 # FIXME should truncate the string here
1046 innerFinished = True
1047 else:
1048 innerFinished = True
1049 if len(newline) > 0:
1050 lines.append(newline)
1051 line = line + 1
1052 else:
1053 finished = True
1054 tmp = []
1055 for i in lines:
1056 tmp.append( fix_rtl( ' '.join(i) ) )
1057 lines = tmp
1058 return lines
1059
1060
1062
1063def paintBackground(image, node):
1064 if node.hasAttribute("bgcolor"):
1065 bgcolor = node.attributes["bgcolor"].value
1066 x = getScaledAttribute(node, "x")
1067 y = getScaledAttribute(node, "y")
1068 w = getScaledAttribute(node, "w")
1069 h = getScaledAttribute(node, "h")
1070 r,g,b = ImageColor.getrgb(bgcolor)
1071
1072 if node.hasAttribute("bgalpha"):
1073 a = int(node.attributes["bgalpha"].value)
1074 else:
1075 a = 255
1076
1077 image.paste((r, g, b, a), (x, y, x + w, y + h))
1078
1079
1080
1082
1083def paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
1084 itemsonthispage, chapternumber, chapterlist):
1085
1086 imagefilename = getThemeFile(themeName, node.attributes["filename"].value)
1087 if not doesFileExist(imagefilename):
1088 fatalError("Cannot find image for menu button (%s)." % imagefilename)
1089 maskimagefilename = getThemeFile(themeName, node.attributes["mask"].value)
1090 if not doesFileExist(maskimagefilename):
1091 fatalError("Cannot find mask image for menu button (%s)." % maskimagefilename)
1092
1093 picture = Image.open(imagefilename,"r").resize(
1094 (getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
1095 picture = picture.convert("RGBA")
1096 bgimage.paste(picture, (getScaledAttribute(node, "x"),
1097 getScaledAttribute(node, "y")), picture)
1098 del picture
1099
1100 # if we have some text paint that over the image
1101 textnode = node.getElementsByTagName("textnormal")
1102 if textnode.length > 0:
1103 textnode = textnode[0]
1104 text = expandItemText(infoDOM,textnode.attributes["value"].value,
1105 itemnum, page, itemsonthispage,
1106 chapternumber,chapterlist)
1107
1108 if text > "":
1109 paintText(draw, bgimage, text, textnode)
1110
1111 del text
1112
1113 write( "Added button image %s" % imagefilename)
1114
1115 picture = Image.open(maskimagefilename,"r").resize(
1116 (getScaledAttribute(node, "w"), getScaledAttribute(node, "h")))
1117 picture = picture.convert("RGBA")
1118 bgimagemask.paste(picture, (getScaledAttribute(node, "x"),
1119 getScaledAttribute(node, "y")),picture)
1120 #del picture
1121
1122 # if we have some text paint that over the image
1123 textnode = node.getElementsByTagName("textselected")
1124 if textnode.length > 0:
1125 textnode = textnode[0]
1126 text = expandItemText(infoDOM, textnode.attributes["value"].value,
1127 itemnum, page, itemsonthispage,
1128 chapternumber, chapterlist)
1129 textImage = Image.new("RGBA",picture.size)
1130 textDraw = ImageDraw.Draw(textImage)
1131
1132 if text > "":
1133 paintText(textDraw, textImage, text, textnode, "white",
1134 getScaledAttribute(node, "x") - getScaledAttribute(textnode, "x"),
1135 getScaledAttribute(node, "y") - getScaledAttribute(textnode, "y"),
1136 getScaledAttribute(textnode, "w"),
1137 getScaledAttribute(textnode, "h"))
1138
1139 #convert the RGB image to a 1 bit image
1140 (width, height) = textImage.size
1141 for y in range(height):
1142 for x in range(width):
1143 if textImage.getpixel((x,y)) < (100, 100, 100, 255):
1144 textImage.putpixel((x,y), (0, 0, 0, 0))
1145 else:
1146 textImage.putpixel((x,y), (255, 255, 255, 255))
1147
1148 if textnode.hasAttribute("colour"):
1149 color = textnode.attributes["colour"].value
1150 elif textnode.hasAttribute("color"):
1151 color = textnode.attributes["color"].value
1152 else:
1153 color = "white"
1154
1155 bgimagemask.paste(color,
1156 (getScaledAttribute(textnode, "x"),
1157 getScaledAttribute(textnode, "y")),
1158 textImage)
1159
1160 del text, textImage, textDraw
1161 del picture
1162
1163
1165
1166def paintText(draw, image, text, node, color = None,
1167 x = None, y = None, width = None, height = None):
1168 """Takes a piece of text and draws it onto an image inside a bounding box."""
1169 #The text is wider than the width of the bounding box
1170
1171 if x is None:
1172 x = getScaledAttribute(node, "x")
1173 y = getScaledAttribute(node, "y")
1174 width = getScaledAttribute(node, "w")
1175 height = getScaledAttribute(node, "h")
1176
1177 font = themeFonts[node.attributes["font"].value]
1178
1179 if color is None:
1180 if node.hasAttribute("colour"):
1181 color = node.attributes["colour"].value
1182 elif node.hasAttribute("color"):
1183 color = node.attributes["color"].value
1184 else:
1185 color = None
1186
1187 if node.hasAttribute("halign"):
1188 halign = node.attributes["halign"].value
1189 elif node.hasAttribute("align"):
1190 halign = node.attributes["align"].value
1191 else:
1192 halign = "left"
1193
1194 if node.hasAttribute("valign"):
1195 valign = node.attributes["valign"].value
1196 else:
1197 valign = "top"
1198
1199 if node.hasAttribute("vindent"):
1200 vindent = int(node.attributes["vindent"].value)
1201 else:
1202 vindent = 0
1203
1204 if node.hasAttribute("hindent"):
1205 hindent = int(node.attributes["hindent"].value)
1206 else:
1207 hindent = 0
1208
1209 lines = intelliDraw(draw, text, font, width - (hindent * 2))
1210 j = 0
1211
1212 # work out what the line spacing should be
1213 textImage = font.drawText(lines[0])
1214 h = int(textImage.size[1] * 1.1)
1215
1216 for i in lines:
1217 if (j * h) < (height - (vindent * 2) - h):
1218 textImage = font.drawText(i, color)
1219 write( "Wrapped text = " + i ) # encoding is done within 'write'
1220
1221 if halign == "left":
1222 xoffset = hindent
1223 elif halign == "center" or halign == "centre":
1224 xoffset = (width // 2) - (textImage.size[0] // 2)
1225 elif halign == "right":
1226 xoffset = width - textImage.size[0] - hindent
1227 else:
1228 xoffset = hindent
1229
1230 if valign == "top":
1231 yoffset = vindent
1232 elif valign == "center" or halign == "centre":
1233 yoffset = (height // 2) - (textImage.size[1] // 2)
1234 elif valign == "bottom":
1235 yoffset = height - textImage.size[1] - vindent
1236 else:
1237 yoffset = vindent
1238
1239 image.paste(textImage, (x + xoffset,y + yoffset + j * h), textImage)
1240 else:
1241 write( "Wrapped text = " + i ) # encoding is done within 'write'
1242 #Move to next line
1243 j = j + 1
1244
1245
1247
1248def paintImage(filename, maskfilename, imageDom, destimage, stretch=True):
1249 """Paste the image specified in the filename into the specified image"""
1250
1251 if not doesFileExist(filename):
1252 write("Image file (%s) does not exist" % filename)
1253 return False
1254
1255 picture = Image.open(filename, "r")
1256 xpos = getScaledAttribute(imageDom, "x")
1257 ypos = getScaledAttribute(imageDom, "y")
1258 w = getScaledAttribute(imageDom, "w")
1259 h = getScaledAttribute(imageDom, "h")
1260 (imgw, imgh) = picture.size
1261 write("Image (%s, %s) into space of (%s, %s) at (%s, %s)" % (imgw, imgh, w, h, xpos, ypos), False)
1262
1263 # the theme can override the default stretch behaviour
1264 if imageDom.hasAttribute("stretch"):
1265 if imageDom.attributes["stretch"].value == "True":
1266 stretch = True
1267 else:
1268 stretch = False
1269
1270 if stretch == True:
1271 imgw = w;
1272 imgh = h;
1273 else:
1274 if float(w)/imgw < float(h)/imgh:
1275 # Width is the constraining dimension
1276 imgh = imgh*w//imgw
1277 imgw = w
1278 if imageDom.hasAttribute("valign"):
1279 valign = imageDom.attributes["valign"].value
1280 else:
1281 valign = "center"
1282
1283 if valign == "bottom":
1284 ypos += h - imgh
1285 if valign == "center":
1286 ypos += (h - imgh)//2
1287 else:
1288 # Height is the constraining dimension
1289 imgw = imgw*h//imgh
1290 imgh = h
1291 if imageDom.hasAttribute("halign"):
1292 halign = imageDom.attributes["halign"].value
1293 else:
1294 halign = "center"
1295
1296 if halign == "right":
1297 xpos += w - imgw
1298 if halign == "center":
1299 xpos += (w - imgw)//2
1300
1301 write("Image resized to (%s, %s) at (%s, %s)" % (imgw, imgh, xpos, ypos), False)
1302 picture = picture.resize((imgw, imgh))
1303 picture = picture.convert("RGBA")
1304
1305 if maskfilename != None and doesFileExist(maskfilename):
1306 maskpicture = Image.open(maskfilename, "r").resize((imgw, imgh))
1307 maskpicture = maskpicture.convert("RGBA")
1308 else:
1309 maskpicture = picture
1310
1311 destimage.paste(picture, (xpos, ypos), maskpicture)
1312 del picture
1313 if maskfilename != None and doesFileExist(maskfilename):
1314 del maskpicture
1315
1316 write ("Added image %s" % filename)
1317
1318 return True
1319
1320
1321
1323
1324def checkBoundaryBox(boundarybox, node):
1325 # We work out how much space all of our graphics and text are taking up
1326 # in a bounding rectangle so that we can use this as an automatic highlight
1327 # on the DVD menu
1328 if getText(node.attributes["static"]) == "False":
1329 if getScaledAttribute(node, "x") < boundarybox[0]:
1330 boundarybox = getScaledAttribute(node, "x"), boundarybox[1], boundarybox[2], boundarybox[3]
1331
1332 if getScaledAttribute(node, "y") < boundarybox[1]:
1333 boundarybox = boundarybox[0], getScaledAttribute(node, "y"), boundarybox[2], boundarybox[3]
1334
1335 if (getScaledAttribute(node, "x") + getScaledAttribute(node, "w")) > boundarybox[2]:
1336 boundarybox = boundarybox[0], boundarybox[1], getScaledAttribute(node, "x") + \
1337 getScaledAttribute(node, "w"), boundarybox[3]
1338
1339 if (getScaledAttribute(node, "y") + getScaledAttribute(node, "h")) > boundarybox[3]:
1340 boundarybox = boundarybox[0], boundarybox[1], boundarybox[2], \
1341 getScaledAttribute(node, "y") + getScaledAttribute(node, "h")
1342
1343 return boundarybox
1344
1345
1347
1348def loadFonts(themeDOM):
1349 global themeFonts
1350
1351 #Find all the fonts
1352 nodelistfonts = themeDOM.getElementsByTagName("font")
1353
1354 fontnumber = 0
1355 for node in nodelistfonts:
1356 filename = getText(node)
1357
1358 if node.hasAttribute("name"):
1359 name = node.attributes["name"].value
1360 else:
1361 name = str(fontnumber)
1362
1363 fontsize = getScaledAttribute(node, "size")
1364
1365 if node.hasAttribute("color"):
1366 color = node.attributes["color"].value
1367 else:
1368 color = "white"
1369
1370 if node.hasAttribute("effect"):
1371 effect = node.attributes["effect"].value
1372 else:
1373 effect = "normal"
1374
1375 if node.hasAttribute("shadowsize"):
1376 shadowsize = int(node.attributes["shadowsize"].value)
1377 else:
1378 shadowsize = 0
1379
1380 if node.hasAttribute("shadowcolor"):
1381 shadowcolor = node.attributes["shadowcolor"].value
1382 else:
1383 shadowcolor = "black"
1384
1385 themeFonts[name] = FontDef(name, getFontPathName(filename),
1386 fontsize, color, effect, shadowcolor, shadowsize)
1387
1388 write( "Loading font %s, %s size %s" % (fontnumber,getFontPathName(filename),fontsize) )
1389 fontnumber+=1
1390
1391
1393
1394def getFileInformation(file, folder):
1395 outputfile = os.path.join(folder, "info.xml")
1396 impl = xml.dom.minidom.getDOMImplementation()
1397 infoDOM = impl.createDocument(None, "fileinfo", None)
1398 top_element = infoDOM.documentElement
1399
1400 data = OrdDict((('chanid',''),
1401 ('type',''), ('filename',''),
1402 ('title',''), ('recordingdate',''),
1403 ('recordingtime',''), ('subtitle',''),
1404 ('description',''), ('rating',''),
1405 ('coverfile',''), ('cutlist','')))
1406
1407 # if the jobfile has amended file details use them
1408 details = file.getElementsByTagName("details")
1409 if details.length > 0:
1410 data.type = file.attributes["type"].value
1411 data.filename = file.attributes["filename"].value
1412 data.title = details[0].attributes["title"].value
1413 data.recordingdate = details[0].attributes["startdate"].value
1414 data.recordingtime = details[0].attributes["starttime"].value
1415 data.subtitle = details[0].attributes["subtitle"].value
1416 data.description = getText(details[0])
1417
1418 # if this a myth recording we still need to find the chanid, starttime and hascutlist
1419 if file.attributes["type"].value=="recording":
1420 filename = file.attributes["filename"].value
1421 try:
1422 rec = next(DB.searchRecorded(basename=os.path.basename(filename)))
1423 except StopIteration:
1424 fatalError("Failed to get recording details from the DB for %s" % filename)
1425
1426 data.chanid = rec.chanid
1427 data.recordingtime = rec.starttime.isoformat()
1428 data.recordingdate = rec.starttime.isoformat()
1429
1430 cutlist = rec.markup.getcutlist()
1431 if len(cutlist):
1432 data.hascutlist = 'yes'
1433 if file.attributes["usecutlist"].value == "0" and addCutlistChapters == True:
1434 chapterlist = ['00:00:00']
1435 res, fps, ar = getVideoParams(folder)
1436 for s,e in cutlist:
1437 chapterlist.append(frameToTime(s, float(fps)))
1438 data.chapterlist = ','.join(chapterlist)
1439 else:
1440 data.hascutlist = 'no'
1441
1442 elif file.attributes["type"].value=="recording":
1443 filename = file.attributes["filename"].value
1444 try:
1445 rec = next(DB.searchRecorded(basename=os.path.basename(filename)))
1446 except StopIteration:
1447 fatalError("Failed to get recording details from the DB for %s" % filename)
1448
1449 write(" " + rec.title)
1450 data.type = file.attributes["type"].value
1451 data.filename = filename
1452 data.title = rec.title
1453 data.recordingdate = rec.progstart.strftime(dateformat)
1454 data.recordingtime = rec.progstart.strftime(timeformat)
1455 data.subtitle = rec.subtitle
1456 data.description = rec.description
1457 data.rating = str(rec.stars)
1458 data.chanid = rec.chanid
1459 data.starttime = rec.starttime.utcisoformat()
1460
1461 cutlist = rec.markup.getcutlist()
1462 if len(cutlist):
1463 data.hascutlist = 'yes'
1464 if file.attributes["usecutlist"].value == "0" and addCutlistChapters == True:
1465 chapterlist = ['00:00:00']
1466 res, fps, ar = getVideoParams(folder)
1467 for s,e in cutlist:
1468 chapterlist.append(frameToTime(s, float(fps)))
1469 data.chapterlist = ','.join(chapterlist)
1470 else:
1471 data.hascutlist = 'no'
1472
1473 elif file.attributes["type"].value=="video":
1474 filename = file.attributes["filename"].value
1475 try:
1476 vid = next(MVID.searchVideos(file=filename))
1477 except StopIteration:
1478 vid = Video.fromFilename(filename)
1479
1480 data.type = file.attributes["type"].value
1481 data.filename = filename
1482 data.title = vid.title
1483
1484 if vid.year != 1895:
1485 data.recordingdate = unicode(vid.year)
1486
1487 data.subtitle = vid.subtitle
1488
1489 if (vid.plot is not None) and (vid.plot != 'None'):
1490 data.description = vid.plot
1491
1492 data.rating = str(vid.userrating)
1493
1494 if doesFileExist(unicode(vid.coverfile)):
1495 data.coverfile = vid.coverfile
1496
1497 elif file.attributes["type"].value=="file":
1498 data.type = file.attributes["type"].value
1499 data.filename = file.attributes["filename"].value
1500 data.title = file.attributes["filename"].value
1501
1502 # if the jobfile has thumb image details copy the images to the work dir
1503 thumbs = file.getElementsByTagName("thumbimages")
1504 if thumbs.length > 0:
1505 thumbs = thumbs[0]
1506 thumbs = file.getElementsByTagName("thumb")
1507 thumblist = []
1508 res, fps, ar = getVideoParams(folder)
1509
1510 for thumb in thumbs:
1511 caption = thumb.attributes["caption"].value
1512 frame = thumb.attributes["frame"].value
1513 filename = thumb.attributes["filename"].value
1514 if caption != "Title":
1515 thumblist.append(frameToTime(int(frame), float(fps)))
1516
1517 # copy thumb file to work dir
1518 copy(filename, folder)
1519
1520 data.thumblist = ','.join(thumblist)
1521
1522 for k,v in list(data.items()):
1523 write( "Node = %s, Data = %s" % (k, v))
1524 node = infoDOM.createElement(k)
1525 # v may be either an integer. Therefore we have to
1526 # convert it to an unicode-string
1527 # If it is already a string it is not encoded as all
1528 # strings in this script are unicode. As no
1529 # encoding-argument is supplied to unicode() it does
1530 # nothing in this case.
1531 node.appendChild(infoDOM.createTextNode(unicode(v)))
1532 top_element.appendChild(node)
1533
1534 WriteXMLToFile (infoDOM, outputfile)
1535
1536
1538
1539def WriteXMLToFile(myDOM, filename):
1540
1541 #Save the XML file to disk for use later on
1542 f=open(filename, 'w')
1543
1544 if sys.hexversion >= 0x03000000:
1545 f.write(myDOM.toprettyxml(indent=" ", encoding="UTF-8").decode())
1546 elif sys.hexversion >= 0x020703F0:
1547 f.write(myDOM.toprettyxml(indent=" ", encoding="UTF-8"))
1548 else:
1549 f.write(myDOM.toxml(encoding="UTF-8"))
1550
1551 f.close()
1552
1553
1554
1556
1557def preProcessFile(file, folder, count):
1558 """Pre-process a single video/recording file."""
1559
1560 write( "Pre-processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
1561
1562 #As part of this routine we need to pre-process the video:
1563 #1. check the file actually exists
1564 #2. extract information from mythtv for this file in xml file
1565 #3. Extract a single frame from the video to use as a thumbnail and resolution check
1566 mediafile=""
1567
1568 if file.attributes["type"].value == "recording":
1569 mediafile = file.attributes["filename"].value
1570 elif file.attributes["type"].value == "video":
1571 mediafile = os.path.join(videopath, file.attributes["filename"].value)
1572 elif file.attributes["type"].value == "file":
1573 mediafile = file.attributes["filename"].value
1574 else:
1575 fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
1576
1577 if file.hasAttribute("localfilename"):
1578 mediafile = file.attributes["localfilename"].value
1579
1580 if doesFileExist(mediafile) == False:
1581 fatalError("Source file does not exist: " + mediafile)
1582
1583 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
1584 copy(os.path.join(folder, "streaminfo.xml"), os.path.join(folder, "streaminfo_orig.xml"))
1585
1586 getFileInformation(file, folder)
1587
1588 videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
1589
1590 write( "Video resolution is %s by %s" % (videosize[0], videosize[1]))
1591
1592
1594
1595def encodeAudio(format, sourcefile, destinationfile, deletesourceafterencode):
1596 write( "Encoding audio to "+format)
1597 if format == "ac3":
1598 cmd = "mythffmpeg -v 0 -y "
1599
1600 if cpuCount > 1:
1601 cmd += "-threads %d " % cpuCount
1602
1603 cmd += "-i %s -f ac3 -ab 192k -ar 48000 %s" % (quoteCmdArg(sourcefile), quoteCmdArg(destinationfile))
1604 result = runCommand(cmd)
1605
1606 if result != 0:
1607 fatalError("Failed while running mythffmpeg to re-encode the audio to ac3\n"
1608 "Command was %s" % cmd)
1609 else:
1610 fatalError("Unknown encodeAudio format " + format)
1611
1612 if deletesourceafterencode==True:
1613 os.remove(sourcefile)
1614
1615
1618
1619def multiplexMPEGStream(video, audio1, audio2, destination, syncOffset):
1620 """multiplex one video and one or two audio streams together"""
1621
1622 write("Multiplexing MPEG stream to %s" % destination)
1623
1624 # no need to use a sync offset if projectx was used to demux the streams
1625 if useprojectx:
1626 syncOffset = 0
1627 else:
1628 if useSyncOffset == True:
1629 write("Adding sync offset of %dms" % syncOffset)
1630 else:
1631 write("Using sync offset is disabled - it would be %dms" % syncOffset)
1632 syncOffset = 0
1633
1634 if doesFileExist(destination)==True:
1635 os.remove(destination)
1636
1637 # figure out what audio files to use
1638 if doesFileExist(audio1 + ".ac3"):
1639 audio1 = audio1 + ".ac3"
1640 elif doesFileExist(audio1 + ".mp2"):
1641 audio1 = audio1 + ".mp2"
1642 else:
1643 fatalError("No audio stream available!")
1644
1645 if doesFileExist(audio2 + ".ac3"):
1646 audio2 = audio2 + ".ac3"
1647 elif doesFileExist(audio2 + ".mp2"):
1648 audio2 = audio2 + ".mp2"
1649
1650 # if subtitles exist, we need to run sequentially, so they can be
1651 # multiplexed to the final file
1652 if os.path.exists(os.path.dirname(destination) + "/stream.d/spumux.xml"):
1653 localUseFIFO=False
1654 else:
1655 localUseFIFO=useFIFO
1656
1657 if localUseFIFO==True:
1658 os.mkfifo(destination)
1659 mode=os.P_NOWAIT
1660 else:
1661 mode=os.P_WAIT
1662
1664
1665 if not doesFileExist(audio2):
1666 write("Available streams - video and one audio stream")
1667 write("running %s -M -f 8 -v 0 --sync-offset %sms -o %s %s %s" %(path_mplex[0], syncOffset, destination, video, audio1))
1668 result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
1669 '-M',
1670 '-f', '8',
1671 '-v', '0',
1672 '--sync-offset', '%sms' % syncOffset,
1673 '-o', destination,
1674 video,
1675 audio1)
1676 else:
1677 write("Available streams - video and two audio streams")
1678 result=os.spawnlp(mode, path_mplex[0], path_mplex[1],
1679 '-M',
1680 '-f', '8',
1681 '-v', '0',
1682 '--sync-offset', '%sms' % syncOffset,
1683 '-o', destination,
1684 video,
1685 audio1,
1686 audio2)
1687
1688 if localUseFIFO == True:
1689 write( "Multiplex started PID=%s" % result)
1690 return result
1691 else:
1692 if result != 0:
1693 fatalError("mplex failed with result %d" % result)
1694
1695 # run spumux to add subtitles if they exist
1696 if os.path.exists(os.path.dirname(destination) + "/stream.d/spumux.xml"):
1697 write("Checking integrity of subtitle pngs")
1698 command = quoteCmdArg(os.path.join(scriptpath, "testsubtitlepngs.sh")) + " " + quoteCmdArg(os.path.dirname(destination) + "/stream.d/spumux.xml")
1699 result = runCommand(command)
1700 if result!=0:
1701 fatalError("Failed while running testsubtitlepngs.sh - %s" % command)
1702
1703 write("Running spumux to add subtitles")
1704 command = quoteCmdArg(path_spumux[0]) + " -P %s <%s >%s" % (quoteCmdArg(os.path.dirname(destination) + "/stream.d/spumux.xml"), quoteCmdArg(destination), quoteCmdArg(os.path.splitext(destination)[0] + "-sub.mpg"))
1705 result = runCommand(command)
1706 if result!=0:
1707 nonfatalError("Failed while running spumux.\n"
1708 "Command was - %s.\n"
1709 "Look in the full log to see why it failed" % command)
1710 os.remove(os.path.splitext(destination)[0] + "-sub.mpg")
1711 else:
1712 os.rename(os.path.splitext(destination)[0] + "-sub.mpg", destination)
1713
1714 return True
1715
1716
1717
1719
1720def getStreamInformation(filename, xmlFilename, lenMethod):
1721 """create a stream.xml file for filename"""
1722
1723 command = "mytharchivehelper -q -q --getfileinfo --infile %s --outfile %s --method %d" % (quoteCmdArg(filename), quoteCmdArg(xmlFilename), lenMethod)
1724
1725
1726 result = runCommand(command)
1727
1728 if result != 0:
1729 fatalError("Failed while running mytharchivehelper to get stream information.\n"
1730 "Result: %d, Command was %s" % (result, command))
1731
1732 # print out the streaminfo.xml file to the log
1733 infoDOM = xml.dom.minidom.parse(xmlFilename)
1734 write(xmlFilename + ":-\n" + infoDOM.toprettyxml(" ", ""), False)
1735
1736
1738
1739def getVideoSize(xmlFilename):
1740 """Get video width and height from stream.xml file"""
1741
1742 #open the XML containing information about this file
1743 infoDOM = xml.dom.minidom.parse(xmlFilename)
1744 #error out if its the wrong XML
1745
1746 if infoDOM.documentElement.tagName != "file":
1747 fatalError("This info file doesn't look right (%s)." % xmlFilename)
1748 nodes = infoDOM.getElementsByTagName("video")
1749 if nodes.length == 0:
1750 fatalError("Didn't find any video elements in stream info file. (%s)" % xmlFilename)
1751
1752 if nodes.length > 1:
1753 write("Found more than one video element in stream info file.!!!")
1754 node = nodes[0]
1755 width = int(node.attributes["width"].value)
1756 height = int(node.attributes["height"].value)
1757
1758 return (width, height)
1759
1760
1762
1763def runMythtranscode(chanid, starttime, destination, usecutlist, localfile):
1764 """Use mythtranscode to cut commercials and/or clean up an mpeg2 file"""
1765
1766 try:
1767 rec = next(DB.searchRecorded(chanid=chanid, starttime=starttime))
1768 cutlist = rec.markup.getcutlist()
1769 except StopIteration:
1770 cutlist = []
1771
1772 cutlist_s = ""
1773 if usecutlist and len(cutlist):
1774 cutlist_s = "'"
1775 for cut in cutlist:
1776 cutlist_s += ' %d-%d ' % cut
1777 cutlist_s += "'"
1778 write("Using cutlist: %s" % cutlist_s)
1779
1780 if (localfile != ""):
1781 if usecutlist == True:
1782 command = "mythtranscode --mpeg2 --honorcutlist %s --infile %s --outfile %s" % (cutlist_s, quoteCmdArg(localfile), quoteCmdArg(destination))
1783 else:
1784 command = "mythtranscode --mpeg2 --infile %s --outfile %s" % (quoteCmdArg(localfile), quoteCmdArg(destination))
1785 else:
1786 if usecutlist == True:
1787 command = "mythtranscode --mpeg2 --honorcutlist --chanid %s --starttime %s --outfile %s" % (chanid, starttime, quoteCmdArg(destination))
1788 else:
1789 command = "mythtranscode --mpeg2 --chanid %s --starttime %s --outfile %s" % (chanid, starttime, quoteCmdArg(destination))
1790
1791 result = runCommand(command)
1792
1793 if (result != 0):
1794 write("Failed while running mythtranscode to cut commercials and/or clean up an mpeg2 file.\n"
1795 "Result: %d, Command was %s" % (result, command))
1796 return False;
1797
1798 return True
1799
1800
1801
1803
1804def generateProjectXCutlist(chanid, starttime, folder):
1805 """generate cutlist_x.txt for ProjectX"""
1806
1807 rec = next(DB.searchRecorded(chanid=chanid, starttime=starttime))
1808 starttime = rec.starttime.utcisoformat()
1809 cutlist = rec.markup.getcutlist()
1810
1811 if len(cutlist):
1812 with open(os.path.join(folder, "cutlist_x.txt"), 'w', encoding='utf-8') as cutlist_f:
1813 cutlist_f.write("CollectionPanel.CutMode=2\n")
1814 i = 0
1815 for cut in cutlist:
1816 # we need to reverse the cutlist because ProjectX wants to know
1817 # the bits to keep not what to cut
1818
1819 if i == 0:
1820 if cut[0] != 0:
1821 cutlist_f.write('0\n%d\n' % cut[0])
1822 cutlist_f.write('%d\n' % cut[1])
1823 elif i == len(cutlist) - 1:
1824 cutlist_f.write('%d\n' % cut[0])
1825 if cut[1] != 9999999:
1826 cutlist_f.write('%d\n9999999\n' % cut[1])
1827 else:
1828 cutlist_f.write('%d\n%d\n' % cut)
1829
1830 i+=1
1831 return True
1832 else:
1833 write("No cutlist in the DB for chanid %s, starttime %s" % chanid, starttime)
1834 return False
1835
1836
1838
1839def runProjectX(chanid, starttime, folder, usecutlist, file):
1840 """Use Project-X to cut commercials and demux an mpeg2 file"""
1841
1842 if usecutlist:
1843 if generateProjectXCutlist(chanid, starttime, folder) == False:
1844 write("Failed to generate Project-X cutlist.")
1845 return False
1846
1847 if os.path.exists(file) != True:
1848 write("Error: input file doesn't exist on local filesystem")
1849 return False
1850
1851 command = quoteCmdArg(path_projectx[0]) + " %s -id '%s' -set ExternPanel.appendPidToFileName=1 -out %s -name stream" % (quoteCmdArg(file), getStreamList(folder), quoteCmdArg(folder))
1852 if usecutlist == True:
1853 command += " -cut %s" % quoteCmdArg(os.path.join(folder, "cutlist_x.txt"))
1854 write(command)
1855 result = runCommand(command)
1856
1857 if (result != 0):
1858 write("Failed while running Project-X to cut commercials and/or demux an mpeg2 file.\n"
1859 "Result: %d, Command was %s" % (result, command))
1860 return False;
1861
1862
1863 # workout which files we need and rename them
1864 video, audio1, audio2 = selectStreams(folder)
1865 if addSubtitles:
1866 subtitles = selectSubtitleStream(folder)
1867
1868 videoID_hex = "0x%x" % video[VIDEO_ID]
1869 if audio1[AUDIO_ID] != -1:
1870 audio1ID_hex = "0x%x" % audio1[AUDIO_ID]
1871 else:
1872 audio1ID_hex = ""
1873 if audio2[AUDIO_ID] != -1:
1874 audio2ID_hex = "0x%x" % audio2[AUDIO_ID]
1875 else:
1876 audio2ID_hex = ""
1877 if addSubtitles and subtitles[SUBTITLE_ID] != -1:
1878 subtitlesID_hex = "0x%x" % subtitles[SUBTITLE_ID]
1879 else:
1880 subtitlesID_hex = ""
1881
1882
1883 files = os.listdir(folder)
1884 for file in files:
1885 if file[0:9] == "stream{0x": # don't rename files that have already been renamed
1886 PID = file[7:13]
1887 SubID = file[19:23]
1888 if PID == videoID_hex or SubID == videoID_hex:
1889 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.mv2"))
1890 elif PID == audio1ID_hex or SubID == audio1ID_hex:
1891 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0." + file[-3:]))
1892 elif PID == audio2ID_hex or SubID == audio2ID_hex:
1893 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1." + file[-3:]))
1894 elif PID == subtitlesID_hex or SubID == subtitlesID_hex:
1895 if file[-3:] == "sup":
1896 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup"))
1897 else:
1898 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup.IFO"))
1899
1900
1901 # Fallback if assignment and renaming by ID failed
1902
1903 files = os.listdir(folder)
1904 for file in files:
1905 if file[0:9] == "stream{0x": # don't rename files that have already been renamed
1906 if not os.path.exists(os.path.join(folder, "stream.mv2")) and file[-3:] == "m2v":
1907 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.mv2"))
1908 elif not (os.path.exists(os.path.join(folder, "stream0.ac3")) or os.path.exists(os.path.join(folder, "stream0.mp2"))) and file[-3:] == "ac3":
1909 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0.ac3"))
1910 elif not (os.path.exists(os.path.join(folder, "stream0.ac3")) or os.path.exists(os.path.join(folder, "stream0.mp2"))) and file[-3:] == "mp2":
1911 os.rename(os.path.join(folder, file), os.path.join(folder, "stream0.mp2"))
1912 elif not (os.path.exists(os.path.join(folder, "stream1.ac3")) or os.path.exists(os.path.join(folder, "stream1.mp2"))) and file[-3:] == "ac3":
1913 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1.ac3"))
1914 elif not (os.path.exists(os.path.join(folder, "stream1.ac3")) or os.path.exists(os.path.join(folder, "stream1.mp2"))) and file[-3:] == "mp2":
1915 os.rename(os.path.join(folder, file), os.path.join(folder, "stream1.mp2"))
1916 elif not os.path.exists(os.path.join(folder, "stream.sup")) and file[-3:] == "sup":
1917 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup"))
1918 elif not os.path.exists(os.path.join(folder, "stream.sup.IFO")) and file[-3:] == "IFO":
1919 os.rename(os.path.join(folder, file), os.path.join(folder, "stream.sup.IFO"))
1920
1921
1922 # if we have some dvb subtitles and the user wants to add them to the DVD
1923 # convert them to pngs and create the spumux xml file
1924 if addSubtitles:
1925 if (os.path.exists(os.path.join(folder, "stream.sup")) and
1926 os.path.exists(os.path.join(folder, "stream.sup.IFO"))):
1927 write("Found DVB subtitles converting to DVD subtitles")
1928 command = "mytharchivehelper -q -q --sup2dast "
1929 command += " --infile %s --ifofile %s --delay 0" % (quoteCmdArg(os.path.join(folder, "stream.sup")), quoteCmdArg(os.path.join(folder, "stream.sup.IFO")))
1930
1931 result = runCommand(command)
1932
1933 if result != 0:
1934 write("Failed while running mytharchivehelper to convert DVB subtitles to DVD subtitles.\n"
1935 "Result: %d, Command was %s" % (result, command))
1936 return False
1937
1938 # sanity check the created spumux.xml
1939 checkSubtitles(os.path.join(folder, "stream.d", "spumux.xml"))
1940
1941 return True
1942
1943
1945
1946def ts2pts(time):
1947 h = int(time[0:2]) * 3600 * 90000
1948 m = int(time[3:5]) * 60 * 90000
1949 s = int(time[6:8]) * 90000
1950 ms = int(time[9:11]) * 90
1951
1952 return h + m + s + ms
1953
1954
1956
1957def checkSubtitles(spumuxFile):
1958
1959 #open the XML containing information about this file
1960 subDOM = xml.dom.minidom.parse(spumuxFile)
1961
1962 #error out if its the wrong XML
1963 if subDOM.documentElement.tagName != "subpictures":
1964 fatalError("This does not look like a spumux.xml file (%s)" % spumuxFile)
1965
1966 streamNodes = subDOM.getElementsByTagName("stream")
1967 if streamNodes.length == 0:
1968 write("Didn't find any stream elements in file.!!!")
1969 return
1970 streamNode = streamNodes[0]
1971
1972 nodes = subDOM.getElementsByTagName("spu")
1973 if nodes.length == 0:
1974 write("Didn't find any spu elements in file.!!!")
1975 return
1976
1977 lastStart = -1
1978 lastEnd = -1
1979 for node in nodes:
1980 errored = False
1981 start = ts2pts(node.attributes["start"].value)
1982 end = ts2pts(node.attributes["end"].value)
1983 image = node.attributes["image"].value
1984
1985 if end <= start:
1986 errored = True
1987 if start <= lastEnd:
1988 errored = True
1989
1990 if errored:
1991 write("removing subtitle: %s to %s - (%d - %d (%d))" % (node.attributes["start"].value, node.attributes["end"].value, start, end, lastEnd), False)
1992 streamNode.removeChild(node)
1993 node.unlink()
1994
1995 lastStart = start
1996 lastEnd = end
1997
1998 WriteXMLToFile(subDOM, spumuxFile)
1999
2000
2002
2003def extractVideoFrame(source, destination, seconds):
2004 write("Extracting thumbnail image from %s at position %s" % (source, seconds))
2005 write("Destination file %s" % destination)
2006
2007 if doesFileExist(destination) == False:
2008
2009 if videomode=="pal":
2010 fr=frameratePAL
2011 else:
2012 fr=framerateNTSC
2013
2014 command = "mytharchivehelper -q -q --createthumbnail --infile %s --thumblist '%s' --outfile %s" % (quoteCmdArg(source), seconds, quoteCmdArg(destination))
2015 result = runCommand(command)
2016 if result != 0:
2017 fatalError("Failed while running mytharchivehelper to get thumbnails.\n"
2018 "Result: %d, Command was %s" % (result, command))
2019 try:
2020 myimage=Image.open(destination,"r")
2021
2022 if myimage.format != "JPEG":
2023 write( "Something went wrong with thumbnail capture - " + myimage.format)
2024 return (long(0),long(0))
2025 else:
2026 return myimage.size
2027 except IOError:
2028 return (long(0),long(0))
2029
2030
2032
2033def extractVideoFrames(source, destination, thumbList):
2034 write("Extracting thumbnail images from: %s - at %s" % (source, thumbList))
2035 write("Destination file %s" % destination)
2036
2037 command = "mytharchivehelper -q -q --createthumbnail --infile %s --thumblist '%s' --outfile %s" % (quoteCmdArg(source), thumbList, quoteCmdArg(destination))
2038 write(command)
2039 result = runCommand(command)
2040 if result != 0:
2041 fatalError("Failed while running mytharchivehelper to get thumbnails.\n"
2042 "Result: %d, Command was %s" % (result, command))
2043
2044
2046
2047def encodeVideoToMPEG2(source, destvideofile, video, audio1, audio2, aspectratio, profile):
2048 """Encodes an unknown video source file eg. AVI to MPEG2 video and AC3 audio, use mythffmpeg"""
2049
2050 profileNode = findEncodingProfile(profile)
2051
2052 passes = int(getText(profileNode.getElementsByTagName("passes")[0]))
2053
2054 command = "mythffmpeg"
2055
2056 if cpuCount > 1:
2057 command += " -threads %d" % cpuCount
2058
2059 parameters = profileNode.getElementsByTagName("parameter")
2060
2061 for param in parameters:
2062 name = param.attributes["name"].value
2063 value = param.attributes["value"].value
2064
2065 # do some parameter substitution
2066 if value == "%inputfile":
2067 value = quoteCmdArg(source)
2068 if value == "%outputfile":
2069 value = quoteCmdArg(destvideofile)
2070 if value == "%aspect":
2071 value = aspectratio
2072
2073 # only re-encode the audio if it is not already in AC3 format
2074 if audio1[AUDIO_CODEC] == "AC3":
2075 if name == "-acodec":
2076 value = "copy"
2077 if name == "-ar" or name == "-b:a" or name == "-ac":
2078 name = ""
2079 value = ""
2080
2081 if name != "":
2082 command += " " + name
2083
2084 if value != "":
2085 command += " " + value
2086
2087
2088 #add second audio track if required
2089 if audio2[AUDIO_ID] != -1:
2090 for param in parameters:
2091 name = param.attributes["name"].value
2092 value = param.attributes["value"].value
2093
2094 # only re-encode the audio if it is not already in AC3 format
2095 if audio1[AUDIO_CODEC] == "AC3":
2096 if name == "-acodec":
2097 value = "copy"
2098 if name == "-ar" or name == "-b:a" or name == "-ac":
2099 name = ""
2100 value = ""
2101
2102 if name == "-acodec" or name == "-ar" or name == "-b:a" or name == "-ac":
2103 command += " " + name + " " + value
2104
2105 #make sure we get the correct stream(s) that we want
2106 command += " -map 0:%d -map 0:%d " % (video[VIDEO_INDEX], audio1[AUDIO_INDEX])
2107 if audio2[AUDIO_ID] != -1:
2108 command += "-map 0:%d" % (audio2[AUDIO_INDEX])
2109
2110 if passes == 1:
2111 write(command)
2112 result = runCommand(command)
2113 if result!=0:
2114 fatalError("Failed while running mythffmpeg to re-encode video.\n"
2115 "Command was %s" % command)
2116
2117 else:
2118 passLog = os.path.join(getTempPath(), 'pass')
2119
2120 pass1 = command.replace("%passno","1")
2121 pass1 = pass1.replace("%passlogfile", quoteCmdArg(passLog))
2122 write("Pass 1 - " + pass1)
2123 result = runCommand(pass1)
2124
2125 if result!=0:
2126 fatalError("Failed while running mythffmpeg (Pass 1) to re-encode video.\n"
2127 "Command was %s" % command)
2128
2129 if os.path.exists(destvideofile):
2130 os.remove(destvideofile)
2131
2132 pass2 = command.replace("%passno","2")
2133 pass2 = pass2.replace("%passlogfile", passLog)
2134 write("Pass 2 - " + pass2)
2135 result = runCommand(pass2)
2136
2137 if result!=0:
2138 fatalError("Failed while running mythffmpeg (Pass 2) to re-encode video.\n"
2139 "Command was %s" % command)
2140
2142
2143def encodeNuvToMPEG2(chanid, starttime, mediafile, destvideofile, folder, profile, usecutlist):
2144 """Encodes a nuv video source file to MPEG2 video and AC3 audio, using mythtranscode & mythffmpeg"""
2145
2146 # make sure mythtranscode hasn't left some stale fifos hanging around
2147 if ((doesFileExist(os.path.join(folder, "audout")) or doesFileExist(os.path.join(folder, "vidout")))):
2148 fatalError("Something is wrong! Found one or more stale fifo's from mythtranscode\n"
2149 "Delete the fifos in '%s' and start again" % folder)
2150
2151 profileNode = findEncodingProfile(profile)
2152 parameters = profileNode.getElementsByTagName("parameter")
2153
2154 # default values - will be overriden by values from the profile
2155 outvideobitrate = "5000k"
2156 if videomode == "ntsc":
2157 outvideores = "720x480"
2158 else:
2159 outvideores = "720x576"
2160
2161 outaudiochannels = 2
2162 outaudiobitrate = 384
2163 outaudiosamplerate = 48000
2164 outaudiocodec = "ac3"
2165 deinterlace = 0
2166 filter = ""
2167 qmin = 5
2168 qmax = 31
2169 qdiff = 31
2170
2171 for param in parameters:
2172 name = param.attributes["name"].value
2173 value = param.attributes["value"].value
2174
2175 # we only support a subset of the parameter for the moment
2176 if name == "-acodec":
2177 outaudiocodec = value
2178 if name == "-ac":
2179 outaudiochannels = value
2180 if name == "-ab":
2181 outaudiobitrate = value
2182 if name == "-ar":
2183 outaudiosamplerate = value
2184 if name == "-b":
2185 outvideobitrate = value
2186 if name == "-s":
2187 outvideores = value
2188 if name == "-deinterlace":
2189 deinterlace = 1
2190 if name == "-filter:v":
2191 filter = " -filter:v " + quoteCmdArg(value) + " "
2192 if name == "-qmin":
2193 qmin = value
2194 if name == "-qmax":
2195 qmax = value
2196 if name == "-qdiff":
2197 qdiff = value
2198
2199 if chanid != -1:
2200 utcstarttime = datetime.duck(starttime).utcisoformat()
2201 if (usecutlist == True):
2202 PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
2203 '--profile', '27',
2204 '--chanid', chanid,
2205 '--starttime', utcstarttime,
2206 '--honorcutlist',
2207 '--fifodir', folder)
2208 write("mythtranscode started (using cut list) PID = %s" % PID)
2209 else:
2210 PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
2211 '--profile', '27',
2212 '--chanid', chanid,
2213 '--starttime', utcstarttime,
2214 '--fifodir', folder)
2215
2216 write("mythtranscode started PID = %s" % PID)
2217 elif mediafile != -1:
2218 PID=os.spawnlp(os.P_NOWAIT, "mythtranscode", "mythtranscode",
2219 '--profile', '27',
2220 '--infile', mediafile,
2221 '--fifodir', folder)
2222 write("mythtranscode started (using file) PID = %s" % PID)
2223 else:
2224 fatalError("no video source passed to encodeNuvToMPEG2.\n")
2225
2226
2227 samplerate, channels = getAudioParams(folder)
2228 videores, fps, aspectratio = getVideoParams(folder)
2229
2230 command = "mythffmpeg -y "
2231
2232 if cpuCount > 1:
2233 command += "-threads %d " % cpuCount
2234
2235 command += "-f s16le -ar %s -ac %s -i %s " % (samplerate, channels, quoteCmdArg(os.path.join(folder, "audout")))
2236 command += "-f rawvideo -pix_fmt yuv420p -s %s -aspect %s -r %s " % (videores, aspectratio, fps)
2237 command += "-i %s " % quoteCmdArg(os.path.join(folder, "vidout"))
2238 command += "-aspect %s -r %s " % (aspectratio, fps)
2239 if (deinterlace == 1):
2240 command += "-deinterlace "
2241 command += "%s" % filter
2242 command += "-s %s -b %s -vcodec mpeg2video " % (outvideores, outvideobitrate)
2243 command += "-qmin %s -qmax %s -qdiff %s " % (qmin, qmax, qdiff)
2244 command += "-ab %s -ar %s -acodec %s " % (outaudiobitrate, outaudiosamplerate, outaudiocodec)
2245 command += "-f dvd %s" % quoteCmdArg(destvideofile)
2246
2247 #wait for mythtranscode to create the fifos
2248 tries = 30
2249 while (tries and not(doesFileExist(os.path.join(folder, "audout")) and
2250 doesFileExist(os.path.join(folder, "vidout")))):
2251 tries -= 1
2252 write("Waiting for mythtranscode to create the fifos")
2253 time.sleep(1)
2254
2255 if (not(doesFileExist(os.path.join(folder, "audout")) and doesFileExist(os.path.join(folder, "vidout")))):
2256 fatalError("Waited too long for mythtranscode to create the fifos - giving up!!")
2257
2258 write("Running mythffmpeg")
2259 result = runCommand(command)
2260 if result != 0:
2261 os.kill(PID, signal.SIGKILL)
2262 fatalError("Failed while running mythffmpeg to re-encode video.\n"
2263 "Command was %s" % command)
2264
2265
2267
2269 write( "Starting dvdauthor")
2271 result=os.spawnlp(os.P_WAIT, path_dvdauthor[0],path_dvdauthor[1],'-x',os.path.join(getTempPath(),'dvdauthor.xml'))
2272 if result!=0:
2273 fatalError("Failed while running dvdauthor. Result: %d" % result)
2274 write( "Finished dvdauthor")
2275
2276
2278
2279def CreateDVDISO(title):
2280 write("Creating ISO image")
2282 command = quoteCmdArg(path_mkisofs[0]) + ' -dvd-video '
2283 command += ' -V ' + quoteCmdArg(title)
2284 command += ' -o ' + quoteCmdArg(os.path.join(getTempPath(), 'mythburn.iso'))
2285 command += " " + quoteCmdArg(os.path.join(getTempPath(),'dvd'))
2286
2287 result = runCommand(command)
2288
2289 if result!=0:
2290 fatalError("Failed while running mkisofs.\n"
2291 "Command was %s" % command)
2292
2293 write("Finished creating ISO image")
2294
2295
2297
2298
2299def BurnDVDISO(title):
2300 write( "Burning ISO image to %s" % dvddrivepath)
2301
2302
2304 def drivestatus():
2305 f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
2306 status = ioctl(f,CDROM.CDROM_DRIVE_STATUS, 0)
2307 os.close(f)
2308 return status
2309 def displayneededdisktype():
2310 if mediatype == DVD_SL:
2311 write("Please insert an empty single-layer disc (DVD+R or DVD-R).")
2312 if mediatype == DVD_DL:
2313 write("Please insert an empty double-layer disc (DVD+R DL or DVD-R DL).")
2314 if mediatype == DVD_RW:
2315 write("Please insert a rewritable disc (DVD+RW or DVD-RW).")
2316 def drive(action, value=0):
2317 waitForDrive()
2318 # workaround as some distros have problems to eject the DVD
2319 # 'sudo sysctl -w dev.cdrom.autoclose=0' should help, but we don't have the privliges to do this
2320 if action == CDROM.CDROMEJECT:
2321 counter = 0
2322 while drivestatus() != CDROM.CDS_TRAY_OPEN and counter < 15:
2323 counter = counter + 1
2324 drive(CDROM.CDROM_LOCKDOOR, 0)
2325 if counter % 2: # if counter is 1,3,5,7,... use ioctl
2326 f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
2327 try:
2328 ioctl(f,action, value)
2329 except:
2330 write("Sending command '0x%x' to drive failed" %action, False)
2331 os.close(f)
2332 else: # try eject-command
2333 if runCommand("eject " + quoteCmdArg(dvddrivepath)) == 32512:
2334 write('"eject" is probably not installed.', False)
2335 waitForDrive()
2336 time.sleep(3)
2337 if drivestatus() == CDROM.CDS_TRAY_OPEN:
2338 res = True
2339 else:
2340 res = False
2341 write("Failed to eject the disc! Probably drive is blocked by another program.")
2342 # normal
2343 else:
2344 f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
2345 try:
2346 ioctl(f,action, value)
2347 res = True
2348 except:
2349 write("Sending command '0x%x' to drive failed" %action, False)
2350 res = False
2351 os.close(f)
2352 return res
2353 def waitForDrive():
2354 tries = 0
2355 while drivestatus() == CDROM.CDS_DRIVE_NOT_READY:
2357 write("Waiting for drive")
2358 time.sleep(5)
2359 runCommand("pumount " + quoteCmdArg(dvddrivepath))
2360 tries += 1
2361 if tries > 10:
2362 # Try a hard reset if the device is still not ready
2363 write("Try a hard-reset of the device")
2364 drive(CDROM.CDROMRESET)
2365 tries = 0
2366
2367
2368
2369
2371
2372
2373 finished = False
2374 while not finished:
2375 # Maybe the user has no appropriate medium or something alike. Give her the chance to cancel.
2377
2378 # If drive needs some time (for example to close the tray) give it to it
2379 waitForDrive()
2380
2381 if drivestatus() == CDROM.CDS_DISC_OK or drivestatus() == CDROM.CDS_NO_INFO:
2382
2383 # If the frontend has a previously burnt DVD+RW mounted,
2384 # growisofs will fail to burn it, so try to pumount it first...
2385 runCommand("pumount " + quoteCmdArg(dvddrivepath));
2386
2387
2388 command = quoteCmdArg(path_growisofs[0]) + " -input-charset=UTF-8 -dvd-compat"
2389 if drivespeed != 0:
2390 command += " -speed=%d" % drivespeed
2391 if mediatype == DVD_RW and erasedvdrw == True:
2392 command += " -use-the-force-luke"
2393 command += " -Z " + quoteCmdArg(dvddrivepath) + " -dvd-video -V " + quoteCmdArg(title) + " " + quoteCmdArg(os.path.join(getTempPath(),'dvd'))
2394 write(command)
2395 write("Running growisofs to burn DVD")
2396
2397 result = runCommand(command)
2398 if result == 0:
2399 finished = True
2400
2401 # Wait till the drive is not busy any longer
2402 f = os.open(dvddrivepath, os.O_RDONLY | os.O_NONBLOCK)
2403 busy = True
2404 tries = 0
2405 while busy and tries < 10:
2406 tries += 1
2407 try:
2408 ioctl(f, CDROM.CDROM_LOCKDOOR, 0)
2409 busy = False
2410 except:
2411 write("Drive is still busy")
2412 time.sleep(5)
2413 waitForDrive()
2414 os.close(f)
2415 else:
2416 if result == 252:
2417 write("-"*60)
2418 write("You probably inserted a medium of wrong type.")
2419 elif (result == 156):
2420 write("-"*60)
2421 write("You probably inserted a non-empty, corrupt or too small medium.")
2422 elif (result == 144):
2423 write("-"*60)
2424 write("You inserted a non-empty medium.")
2425 else:
2426 write("-"*60)
2427 write("ERROR: Failed while running growisofs.")
2428 write("Result %d, Command was: %s" % (result, command))
2429 write("Please check mythburn.log for further information")
2430 write("-"*60)
2431 write("")
2432 write("Going to try it again until canceled by user:")
2433 write("-"*60)
2434 write("")
2435 displayneededdisktype()
2436
2437 # eject the disc
2438 drive(CDROM.CDROMEJECT)
2439
2440
2441 elif drivestatus() == CDROM.CDS_TRAY_OPEN:
2442 displayneededdisktype()
2443 write("Waiting for tray to close.")
2444 # Wait until user closes tray or cancels
2445 while drivestatus() == CDROM.CDS_TRAY_OPEN:
2447 time.sleep(5)
2448 elif drivestatus() == CDROM.CDS_NO_DISC:
2449 drive(CDROM.CDROMEJECT)
2450 displayneededdisktype()
2451
2452 write("Finished burning ISO image")
2453
2454
2457
2458def deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2):
2459
2460 if getFileType(folder) == "mpegts":
2461 command = "mythreplex --demux --fix_sync -t TS -o %s " % quoteCmdArg(folder + "/stream")
2462 command += "-v %d " % (video[VIDEO_ID])
2463
2464 if audio1[AUDIO_ID] != -1:
2465 if audio1[AUDIO_CODEC] == 'MP2':
2466 command += "-a %d " % (audio1[AUDIO_ID])
2467 elif audio1[AUDIO_CODEC] == 'AC3':
2468 command += "-c %d " % (audio1[AUDIO_ID])
2469 elif audio1[AUDIO_CODEC] == 'EAC3':
2470 command += "-c %d " % (audio1[AUDIO_ID])
2471
2472 if audio2[AUDIO_ID] != -1:
2473 if audio2[AUDIO_CODEC] == 'MP2':
2474 command += "-a %d " % (audio2[AUDIO_ID])
2475 elif audio2[AUDIO_CODEC] == 'AC3':
2476 command += "-c %d " % (audio2[AUDIO_ID])
2477 elif audio2[AUDIO_CODEC] == 'EAC3':
2478 command += "-c %d " % (audio2[AUDIO_ID])
2479
2480 else:
2481 command = "mythreplex --demux --fix_sync -o %s " % quoteCmdArg(folder + "/stream")
2482 command += "-v %d " % (video[VIDEO_ID] & 255)
2483
2484 if audio1[AUDIO_ID] != -1:
2485 if audio1[AUDIO_CODEC] == 'MP2':
2486 command += "-a %d " % (audio1[AUDIO_ID] & 255)
2487 elif audio1[AUDIO_CODEC] == 'AC3':
2488 command += "-c %d " % (audio1[AUDIO_ID] & 255)
2489 elif audio1[AUDIO_CODEC] == 'EAC3':
2490 command += "-c %d " % (audio1[AUDIO_ID] & 255)
2491
2492
2493 if audio2[AUDIO_ID] != -1:
2494 if audio2[AUDIO_CODEC] == 'MP2':
2495 command += "-a %d " % (audio2[AUDIO_ID] & 255)
2496 elif audio2[AUDIO_CODEC] == 'AC3':
2497 command += "-c %d " % (audio2[AUDIO_ID] & 255)
2498 elif audio2[AUDIO_CODEC] == 'EAC3':
2499 command += "-c %d " % (audio2[AUDIO_ID] & 255)
2500
2501 mediafile = quoteCmdArg(mediafile)
2502 command += mediafile
2503 write("Running: " + command)
2504
2505 result = runCommand(command)
2506
2507 if result!=0:
2508 fatalError("Failed while running mythreplex. Command was %s" % command)
2509
2510
2512
2513def runM2VRequantiser(source,destination,factor):
2514 mega=1024.0*1024.0
2515 M2Vsize0 = os.path.getsize(source)
2516 write("Initial M2Vsize is %.2f Mb , target is %.2f Mb" % ( (float(M2Vsize0)/mega), (float(M2Vsize0)/(factor*mega)) ))
2517
2518 command = quoteCmdArg(path_M2VRequantiser[0])
2519 command += " %.5f " % factor
2520 command += " %s " % M2Vsize0
2521 command += " < %s " % quoteCmdArg(source)
2522 command += " > %s " % quoteCmdArg(destination)
2523
2524 write("Running: " + command)
2525 result = runCommand(command)
2526 if result!=0:
2527 fatalError("Failed while running M2VRequantiser. Command was %s" % command)
2528
2529 M2Vsize1 = os.path.getsize(destination)
2530
2531 write("M2Vsize after requant is %.2f Mb " % (float(M2Vsize1)/mega))
2532 fac1=float(M2Vsize0) / float(M2Vsize1)
2533 write("Factor demanded %.5f, achieved %.5f, ratio %.5f " % ( factor, fac1, fac1/factor))
2534
2535
2537
2539 """ Returns the sizes of all video, audio and menu files"""
2540 filecount=0
2541 totalvideosize=0
2542 totalaudiosize=0
2543 totalmenusize=0
2544
2545 for node in files:
2546 filecount+=1
2547 #Generate a temp folder name for this file
2548 folder=getItemTempPath(filecount)
2549 #Process this file
2550 file=os.path.join(folder,"stream.mv2")
2551 #Get size of vobfile in MBytes
2552 totalvideosize+=os.path.getsize(file)
2553
2554 #Get size of audio track 1
2555 if doesFileExist(os.path.join(folder,"stream0.ac3")):
2556 totalaudiosize+=os.path.getsize(os.path.join(folder,"stream0.ac3"))
2557 if doesFileExist(os.path.join(folder,"stream0.mp2")):
2558 totalaudiosize+=os.path.getsize(os.path.join(folder,"stream0.mp2"))
2559
2560 #Get size of audio track 2 if available
2561 if doesFileExist(os.path.join(folder,"stream1.ac3")):
2562 totalaudiosize+=os.path.getsize(os.path.join(folder,"stream1.ac3"))
2563 if doesFileExist(os.path.join(folder,"stream1.mp2")):
2564 totalaudiosize+=os.path.getsize(os.path.join(folder,"stream1.mp2"))
2565
2566 # add chapter menu if available
2567 if doesFileExist(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount)):
2568 totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"chaptermenu-%s.mpg" % filecount))
2569
2570 # add details page if available
2571 if doesFileExist(os.path.join(getTempPath(),"details-%s.mpg" % filecount)):
2572 totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"details-%s.mpg" % filecount))
2573
2574 filecount=1
2575 while doesFileExist(os.path.join(getTempPath(),"menu-%s.mpg" % filecount)):
2576 totalmenusize+=os.path.getsize(os.path.join(getTempPath(),"menu-%s.mpg" % filecount))
2577 filecount+=1
2578
2579 return totalvideosize,totalaudiosize,totalmenusize
2580
2581
2583
2584def total_mv2_brl(files,rate):
2585 tvsize=0
2586 filecount=0
2587 for node in files:
2588 filecount+=1
2589 folder=getItemTempPath(filecount)
2590 progduration=getLengthOfVideo(filecount)
2591 file=os.path.join(folder,"stream.mv2")
2592 progvsize=os.path.getsize(file)
2593 progvbitrate=progvsize/progduration
2594 if progvbitrate>rate :
2595 tvsize+=progduration*rate
2596 else:
2597 tvsize+=progvsize
2598
2599 return tvsize
2600
2601
2604
2605def performMPEG2Shrink(files,dvdrsize):
2607 mega=1024.0*1024.0
2608 fudge_pack=1.04 # for mpeg packing
2609 fudge_requant=1.05 # for requant shrinkage uncertainty
2610
2611 totalvideosize,totalaudiosize,totalmenusize=calculateFileSizes(files)
2612 allfiles=totalvideosize+totalaudiosize+totalmenusize
2613
2614 #Report findings
2615 write( "Total video %.2f Mb, audio %.2f Mb, menus %.2f Mb." % (totalvideosize/mega,totalaudiosize/mega,totalmenusize/mega))
2616
2617 #Subtract the audio, menus and packaging overhead from the size of the disk (we cannot shrink this further)
2618 mv2space=((dvdrsize*mega-totalmenusize)/fudge_pack)-totalaudiosize
2619
2620 if mv2space<0:
2621 fatalError("Audio and menu files are too big. No room for video. Giving up!")
2622
2623 if totalvideosize>mv2space:
2624 write( "Video files are %.1f Mb too big. Need to shrink." % ((totalvideosize - mv2space)/mega) )
2625
2626 if path_M2VRequantiser[0] == "":
2627 fatalError("M2VRequantiser is not available to resize the files. Giving up!")
2628
2629 vsize=0
2630 duration=0
2631 filecount=0
2632 for node in files:
2633 filecount+=1
2634 folder=getItemTempPath(filecount)
2635 file=os.path.join(folder,"stream.mv2")
2636 vsize+=os.path.getsize(file)
2637 duration+=getLengthOfVideo(filecount)
2638
2639 #We need to shrink the video files to fit into the space available. It seems sensible
2640 #to do this by imposing a common upper limit on the mean video bit-rate of each recording;
2641 #this will not further reduce the visual quality of any that were transmitted at lower bit-rates.
2642
2643 #Now find the bit-rate limit by iteration between initially defined upper and lower bounds.
2644 #The code is based on 'rtbis' from Numerical Recipes by W H Press et al., CUP.
2645
2646 #A small multiple of the average input bit-rate should be ok as the initial upper bound,
2647 #(although a fixed value or one related to the max value could be used), and zero as the lower bound.
2648 #The function relating bit-rate upper limit to total file size is smooth and monotonic,
2649 #so there should be no convergence problem.
2650
2651 vrLo=0.0
2652 vrHi=3.0*float(vsize)/duration
2653
2654 vrate=vrLo
2655 vrinc=vrHi-vrLo
2656 count=0
2657
2658 while count<30 :
2659 count+=1
2660 vrinc=vrinc*0.5
2661 vrtest=vrate+vrinc
2662 testsize=total_mv2_brl(files,vrtest)
2663 if (testsize<mv2space):
2664 vrate=vrtest
2665
2666 write("vrate %.3f kb/s, testsize %.4f , mv2space %.4f Mb " % ((vrate)/1000.0, (testsize)/mega, (mv2space)/mega) )
2667 filecount=0
2668 for node in files:
2669 filecount+=1
2670 folder=getItemTempPath(filecount)
2671 file=os.path.join(folder,"stream.mv2")
2672 progvsize=os.path.getsize(file)
2673 progduration=getLengthOfVideo(filecount)
2674 progvbitrate=progvsize/progduration
2675 write( "File %s, size %.2f Mb, rate %.2f, limit %.2f kb/s " %( filecount, float(progvsize)/mega, progvbitrate/1000.0, vrate/1000.0 ))
2676 if progvbitrate>vrate :
2677 scalefactor=1.0+(fudge_requant*float(progvbitrate-vrate)/float(vrate))
2678 if scalefactor>3.0 :
2679 write( "Large shrink factor. You may not like the result! ")
2680 runM2VRequantiser(os.path.join(getItemTempPath(filecount),"stream.mv2"),os.path.join(getItemTempPath(filecount),"stream.small.mv2"),scalefactor)
2681 os.remove(os.path.join(getItemTempPath(filecount),"stream.mv2"))
2682 os.rename(os.path.join(getItemTempPath(filecount),"stream.small.mv2"),os.path.join(getItemTempPath(filecount),"stream.mv2"))
2683 else:
2684 write( "Unpackaged total %.2f Mb. About %.0f Mb will be unused." % ((allfiles/mega),(mv2space-totalvideosize)/mega))
2685
2686
2688
2689def createDVDAuthorXML(screensize, numberofitems):
2690 """Creates the xml file for dvdauthor to use the MythBurn menus."""
2691
2692 #Get the main menu node (we must only have 1)
2693 menunode=themeDOM.getElementsByTagName("menu")
2694 if menunode.length!=1:
2695 fatalError("Cannot find the menu element in the theme file")
2696 menunode=menunode[0]
2697
2698 menuitems=menunode.getElementsByTagName("item")
2699 #Total number of video items on a single menu page (no less than 1!)
2700 itemsperpage = menuitems.length
2701 write( "Menu items per page %s" % itemsperpage)
2702 autoplaymenu = 2 + ((numberofitems + itemsperpage - 1)//itemsperpage)
2703
2704 if wantChapterMenu:
2705 #Get the chapter menu node (we must only have 1)
2706 submenunode=themeDOM.getElementsByTagName("submenu")
2707 if submenunode.length!=1:
2708 fatalError("Cannot find the submenu element in the theme file")
2709
2710 submenunode=submenunode[0]
2711
2712 chapteritems=submenunode.getElementsByTagName("chapter")
2713 #Total number of video items on a single menu page (no less than 1!)
2714 chapters = chapteritems.length
2715 write( "Chapters per recording %s" % chapters)
2716
2717 del chapteritems
2718 del submenunode
2719
2720 #Page number counter
2721 page=1
2722
2723 #Item counter to indicate current video item
2724 itemnum=1
2725
2726 write( "Creating DVD XML file for dvd author")
2727
2728 dvddom = xml.dom.minidom.parseString(
2729 '''<dvdauthor>
2730 <vmgm>
2731 <menus lang="en">
2732 <pgc entry="title">
2733 </pgc>
2734 </menus>
2735 </vmgm>
2736 </dvdauthor>''')
2737
2738 dvdauthor_element=dvddom.documentElement
2739 menus_element = dvdauthor_element.childNodes[1].childNodes[1]
2740
2741 dvdauthor_element.insertBefore( dvddom.createComment("""
2742 DVD Variables
2743 g0=not used
2744 g1=not used
2745 g2=title number selected on current menu page (see g4)
2746 g3=1 if intro movie has played
2747 g4=last menu page on display
2748 g5=next title to autoplay (0 or > # titles means no more autoplay)
2749 """), dvdauthor_element.firstChild )
2750 dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
2751
2752 menus_element.appendChild( dvddom.createComment("Title menu used to hold intro movie") )
2753
2754 dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
2755
2756 video = dvddom.createElement("video")
2757 video.setAttribute("format",videomode)
2758
2759 # set aspect ratio
2760 if mainmenuAspectRatio == "4:3":
2761 video.setAttribute("aspect", "4:3")
2762 else:
2763 video.setAttribute("aspect", "16:9")
2764 video.setAttribute("widescreen", "nopanscan")
2765
2766 menus_element.appendChild(video)
2767
2768 pgc=menus_element.childNodes[1]
2769
2770 if wantIntro:
2771 #code to skip over intro if its already played
2772 pre = dvddom.createElement("pre")
2773 pgc.appendChild(pre)
2774 vmgm_pre_node=pre
2775 del pre
2776
2777 node = themeDOM.getElementsByTagName("intro")[0]
2778 introFile = node.attributes["filename"].value
2779
2780 #Pick the correct intro movie based on video format ntsc/pal
2781 vob = dvddom.createElement("vob")
2782 vob.setAttribute("file",os.path.join(getThemeFile(themeName, videomode + '_' + introFile)))
2783 pgc.appendChild(vob)
2784 del vob
2785
2786 #We use g3 to indicate that the intro has been played at least once
2787 #default g2 to point to first recording
2788 post = dvddom.createElement("post")
2789 post .appendChild(dvddom.createTextNode("{g3=1;g2=1;jump menu 2;}"))
2790 pgc.appendChild(post)
2791 del post
2792 else:
2793 # If there's no intro, we need to jump to the next menu
2794 post = dvddom.createElement("post")
2795 post .appendChild(dvddom.createTextNode("{g3=1;g2=1;jump menu 2;}"))
2796 pgc.appendChild(post)
2797 del post
2798
2799 while itemnum <= numberofitems:
2800 write( "Menu page %s" % page)
2801
2802 #For each menu page we need to create a new PGC structure
2803 menupgc = dvddom.createElement("pgc")
2804 menus_element.appendChild(menupgc)
2805
2806 menupgc.appendChild( dvddom.createComment("Menu Page %s" % page) )
2807
2808 #Make sure the button last highlighted is selected
2809 #g4 holds the menu page last displayed
2810 pre = dvddom.createElement("pre")
2811 pre.appendChild(dvddom.createTextNode("{button=g2*1024;g4=%s;}" % page))
2812 menupgc.appendChild(pre)
2813
2814 vob = dvddom.createElement("vob")
2815 vob.setAttribute("file",os.path.join(getTempPath(),"menu-%s.mpg" % page))
2816 menupgc.appendChild(vob)
2817
2818 #Loop menu forever
2819 post = dvddom.createElement("post")
2820 post.appendChild(dvddom.createTextNode("jump cell 1;"))
2821 menupgc.appendChild(post)
2822
2823 #Default settings for this page
2824
2825 #Number of video items on this menu page
2826 itemsonthispage=0
2827
2828 endbuttons = []
2829 #Loop through all the items on this menu page
2830 while itemnum <= numberofitems and itemsonthispage < itemsperpage:
2831 menuitem=menuitems[ itemsonthispage ]
2832
2833 itemsonthispage+=1
2834
2835 #Get the XML containing information about this item
2836 infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(itemnum),"info.xml") )
2837 #Error out if its the wrong XML
2838 if infoDOM.documentElement.tagName != "fileinfo":
2839 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(getItemTempPath(itemnum),"info.xml"))
2840
2841 #write( themedom.toprettyxml())
2842
2843 #Add this recording to this page's menu...
2844 button=dvddom.createElement("button")
2845 button.setAttribute("name","%s" % itemnum)
2846 button.appendChild(dvddom.createTextNode("{g2=" + "%s" % itemsonthispage + "; g5=0; jump title %s;}" % itemnum))
2847 menupgc.appendChild(button)
2848 del button
2849
2850 #Create a TITLESET for each item
2851 titleset = dvddom.createElement("titleset")
2852 dvdauthor_element.appendChild(titleset)
2853
2854 #Comment XML file with title of video
2855 comment = getText(infoDOM.getElementsByTagName("title")[0]).replace('--', '-')
2856 titleset.appendChild( dvddom.createComment(comment))
2857
2858 menus= dvddom.createElement("menus")
2859 titleset.appendChild(menus)
2860
2861 video = dvddom.createElement("video")
2862 video.setAttribute("format",videomode)
2863
2864 # set the right aspect ratio
2865 if chaptermenuAspectRatio == "4:3":
2866 video.setAttribute("aspect", "4:3")
2867 elif chaptermenuAspectRatio == "16:9":
2868 video.setAttribute("aspect", "16:9")
2869 video.setAttribute("widescreen", "nopanscan")
2870 else:
2871 # use same aspect ratio as the video
2872 if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
2873 video.setAttribute("aspect", "16:9")
2874 video.setAttribute("widescreen", "nopanscan")
2875 else:
2876 video.setAttribute("aspect", "4:3")
2877
2878 menus.appendChild(video)
2879
2880 if wantChapterMenu:
2881 mymenupgc = dvddom.createElement("pgc")
2882 menus.appendChild(mymenupgc)
2883
2884 pre = dvddom.createElement("pre")
2885 mymenupgc.appendChild(pre)
2886 if wantDetailsPage:
2887 pre.appendChild(dvddom.createTextNode("{button=s7 - 1 * 1024;}"))
2888 else:
2889 pre.appendChild(dvddom.createTextNode("{button=s7 * 1024;}"))
2890
2891 vob = dvddom.createElement("vob")
2892 vob.setAttribute("file",os.path.join(getTempPath(),"chaptermenu-%s.mpg" % itemnum))
2893 mymenupgc.appendChild(vob)
2894
2895 #Loop menu forever
2896 post = dvddom.createElement("post")
2897 post.appendChild(dvddom.createTextNode("jump cell 1;"))
2898 mymenupgc.appendChild(post)
2899
2900 # the first chapter MUST be 00:00:00 if its not dvdauthor adds it which
2901 # throws of the chapter selection - so make sure we add it if needed so we
2902 # can compensate for it in the chapter selection menu
2903 firstChapter = 0
2904 thumblist = createVideoChapters(itemnum, chapters, getLengthOfVideo(itemnum), False)
2905 chapterlist = thumblist.split(",")
2906 if chapterlist[0] != '00:00:00':
2907 firstChapter = 1
2908 x = 1
2909 while x <= chapters:
2910 #Add this recording to this page's menu...
2911 button = dvddom.createElement("button")
2912 button.setAttribute("name","%s" % x)
2913 if wantDetailsPage:
2914 button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, firstChapter + x + 1)))
2915 else:
2916 button.appendChild(dvddom.createTextNode("jump title %s chapter %s;" % (1, firstChapter + x)))
2917
2918 mymenupgc.appendChild(button)
2919 del button
2920 x += 1
2921
2922 #add the titlemenu button if required
2923 submenunode = themeDOM.getElementsByTagName("submenu")
2924 submenunode = submenunode[0]
2925 titlemenunodes = submenunode.getElementsByTagName("titlemenu")
2926 if titlemenunodes.length > 0:
2927 button = dvddom.createElement("button")
2928 button.setAttribute("name","titlemenu")
2929 button.appendChild(dvddom.createTextNode("{jump vmgm menu;}"))
2930 mymenupgc.appendChild(button)
2931 del button
2932
2933 titles = dvddom.createElement("titles")
2934 titleset.appendChild(titles)
2935
2936 # set the right aspect ratio
2937 title_video = dvddom.createElement("video")
2938 title_video.setAttribute("format",videomode)
2939
2940 if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
2941 title_video.setAttribute("aspect", "16:9")
2942 title_video.setAttribute("widescreen", "nopanscan")
2943 else:
2944 title_video.setAttribute("aspect", "4:3")
2945
2946 titles.appendChild(title_video)
2947
2948 #set right audio format
2949 if doesFileExist(os.path.join(getItemTempPath(itemnum), "stream0.mp2")):
2950 title_audio = dvddom.createElement("audio")
2951 title_audio.setAttribute("format", "mp2")
2952 else:
2953 title_audio = dvddom.createElement("audio")
2954 title_audio.setAttribute("format", "ac3")
2955
2956 titles.appendChild(title_audio)
2957
2958 pgc = dvddom.createElement("pgc")
2959 titles.appendChild(pgc)
2960
2961 if wantDetailsPage:
2962 #add the detail page intro for this item
2963 vob = dvddom.createElement("vob")
2964 vob.setAttribute("file",os.path.join(getTempPath(),"details-%s.mpg" % itemnum))
2965 pgc.appendChild(vob)
2966
2967 vob = dvddom.createElement("vob")
2968 if wantChapterMenu:
2969 thumblist = createVideoChapters(itemnum, chapters, getLengthOfVideo(itemnum), False)
2970 chapterlist = thumblist.split(",")
2971 if chapterlist[0] != '00:00:00':
2972 thumblist = '00:00:00,' + thumblist
2973 vob.setAttribute("chapters", thumblist)
2974 else:
2975 vob.setAttribute("chapters",
2977 chapterLength,
2978 getLengthOfVideo(itemnum)))
2979
2980 vob.setAttribute("file",os.path.join(getItemTempPath(itemnum),"final.vob"))
2981 pgc.appendChild(vob)
2982
2983 post = dvddom.createElement("post")
2984 post.appendChild(dvddom.createTextNode("if (g5 eq %s) call vmgm menu %s; call vmgm menu %s;" % (itemnum + 1, autoplaymenu, page + 1)))
2985 pgc.appendChild(post)
2986
2987 #Quick variable tidy up (not really required under Python)
2988 del titleset
2989 del titles
2990 del menus
2991 del video
2992 del pgc
2993 del vob
2994 del post
2995
2996 #Loop through all the nodes inside this menu item and pick previous / next buttons
2997 for node in menuitem.childNodes:
2998
2999 if node.nodeName=="previous":
3000 if page>1:
3001 button=dvddom.createElement("button")
3002 button.setAttribute("name","previous")
3003 button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % page ))
3004 endbuttons.append(button)
3005
3006
3007 elif node.nodeName=="next":
3008 if itemnum < numberofitems:
3009 button=dvddom.createElement("button")
3010 button.setAttribute("name","next")
3011 button.appendChild(dvddom.createTextNode("{g2=1;jump menu %s;}" % (page + 2)))
3012 endbuttons.append(button)
3013
3014 elif node.nodeName=="playall":
3015 button=dvddom.createElement("button")
3016 button.setAttribute("name","playall")
3017 button.appendChild(dvddom.createTextNode("{g5=1; jump menu %s;}" % autoplaymenu))
3018 endbuttons.append(button)
3019
3020 #On to the next item
3021 itemnum+=1
3022
3023 #Move on to the next page
3024 page+=1
3025
3026 for button in endbuttons:
3027 menupgc.appendChild(button)
3028 del button
3029
3030 menupgc = dvddom.createElement("pgc")
3031 menus_element.appendChild(menupgc)
3032 menupgc.setAttribute("pause","inf")
3033 menupgc.appendChild( dvddom.createComment("Autoplay hack") )
3034
3035 dvdcode = ""
3036 while (itemnum > 1):
3037 itemnum-=1
3038 dvdcode += "if (g5 eq %s) {g5 = %s; jump title %s;} " % (itemnum, itemnum + 1, itemnum)
3039 dvdcode += "g5 = 0; jump menu 1;"
3040
3041 pre = dvddom.createElement("pre")
3042 pre.appendChild(dvddom.createTextNode(dvdcode))
3043 menupgc.appendChild(pre)
3044
3045 if wantIntro:
3046 #Menu creation is finished so we know how many pages were created
3047 #add to to jump to the correct one automatically
3048 dvdcode="if (g3 eq 1) {"
3049 while (page>1):
3050 page-=1;
3051 dvdcode+="if (g4 eq %s) " % page
3052 dvdcode+="jump menu %s;" % (page + 1)
3053 if (page>1):
3054 dvdcode+=" else "
3055 dvdcode+="}"
3056 vmgm_pre_node.appendChild(dvddom.createTextNode(dvdcode))
3057
3058 #write(dvddom.toprettyxml())
3059 #Save xml to file
3060 WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
3061
3062 #Destroy the DOM and free memory
3063 dvddom.unlink()
3064
3065
3067
3068def createDVDAuthorXMLNoMainMenu(screensize, numberofitems):
3069 """Creates the xml file for dvdauthor to use the MythBurn menus."""
3070
3071 # creates a simple DVD with only a chapter menus shown before each video
3072 # can contain an intro movie and each title can have a details page
3073 # displayed before each title
3074
3075 write( "Creating DVD XML file for dvd author (No Main Menu)")
3076 #FIXME:
3077 assert False
3078
3079
3081
3082def createDVDAuthorXMLNoMenus(screensize, numberofitems):
3083 """Creates the xml file for dvdauthor containing no menus."""
3084
3085 # creates a simple DVD with no menus that chains the videos one after the other
3086 # can contain an intro movie and each title can have a details page
3087 # displayed before each title
3088
3089 write( "Creating DVD XML file for dvd author (No Menus)")
3090
3091 dvddom = xml.dom.minidom.parseString(
3092 '''
3093 <dvdauthor>
3094 <vmgm>
3095 <menus lang="en">
3096 <pgc entry="title" pause="0">
3097 </pgc>
3098 </menus>
3099 </vmgm>
3100 </dvdauthor>''')
3101
3102 dvdauthor_element = dvddom.documentElement
3103 menus = dvdauthor_element.childNodes[1].childNodes[1]
3104 menu_pgc = menus.childNodes[1]
3105
3106 dvdauthor_element.insertBefore(dvddom.createComment("dvdauthor XML file created by MythBurn script"), dvdauthor_element.firstChild )
3107 dvdauthor_element.setAttribute("dest",os.path.join(getTempPath(),"dvd"))
3108
3109 # create pgc for menu 1 holds the intro if required, blank mpg if not
3110 if wantIntro:
3111 video = dvddom.createElement("video")
3112 video.setAttribute("format", videomode)
3113
3114 # set aspect ratio
3115 if mainmenuAspectRatio == "4:3":
3116 video.setAttribute("aspect", "4:3")
3117 else:
3118 video.setAttribute("aspect", "16:9")
3119 video.setAttribute("widescreen", "nopanscan")
3120 menus.appendChild(video)
3121
3122 pre = dvddom.createElement("pre")
3123 pre.appendChild(dvddom.createTextNode("if (g2==1) jump menu 2;"))
3124 menu_pgc.appendChild(pre)
3125
3126 node = themeDOM.getElementsByTagName("intro")[0]
3127 introFile = node.attributes["filename"].value
3128
3129 vob = dvddom.createElement("vob")
3130 vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + introFile))
3131 menu_pgc.appendChild(vob)
3132
3133 post = dvddom.createElement("post")
3134 post.appendChild(dvddom.createTextNode("g2=1; jump menu 2;"))
3135 menu_pgc.appendChild(post)
3136 del menu_pgc
3137 del post
3138 del pre
3139 del vob
3140 else:
3141 pre = dvddom.createElement("pre")
3142 pre.appendChild(dvddom.createTextNode("g2=1;jump menu 2;"))
3143 menu_pgc.appendChild(pre)
3144
3145 vob = dvddom.createElement("vob")
3146 vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
3147 menu_pgc.appendChild(vob)
3148
3149 del menu_pgc
3150 del pre
3151 del vob
3152
3153 # create menu 2 - dummy menu that allows us to jump to each titleset in sequence
3154 menu_pgc = dvddom.createElement("pgc")
3155
3156 preText = "if (g1==0) g1=1;"
3157 for i in range(numberofitems):
3158 preText += "if (g1==%d) jump titleset %d menu;" % (i + 1, i + 1)
3159
3160 pre = dvddom.createElement("pre")
3161 pre.appendChild(dvddom.createTextNode(preText))
3162 menu_pgc.appendChild(pre)
3163
3164 vob = dvddom.createElement("vob")
3165 vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
3166 menu_pgc.appendChild(vob)
3167 menus.appendChild(menu_pgc)
3168
3169 # for each title add a <titleset> section
3170 itemNum = 1
3171 while itemNum <= numberofitems:
3172 write( "Adding item %s" % itemNum)
3173
3174 titleset = dvddom.createElement("titleset")
3175 dvdauthor_element.appendChild(titleset)
3176
3177 # create menu
3178 menu = dvddom.createElement("menus")
3179 menupgc = dvddom.createElement("pgc")
3180 menu.appendChild(menupgc)
3181 titleset.appendChild(menu)
3182
3183 if wantDetailsPage:
3184 #add the detail page intro for this item
3185 vob = dvddom.createElement("vob")
3186 vob.setAttribute("file", os.path.join(getTempPath(),"details-%s.mpg" % itemNum))
3187 menupgc.appendChild(vob)
3188
3189 post = dvddom.createElement("post")
3190 post.appendChild(dvddom.createTextNode("jump title 1;"))
3191 menupgc.appendChild(post)
3192 del post
3193 else:
3194 #add dummy menu for this item
3195 pre = dvddom.createElement("pre")
3196 pre.appendChild(dvddom.createTextNode("jump title 1;"))
3197 menupgc.appendChild(pre)
3198 del pre
3199
3200 vob = dvddom.createElement("vob")
3201 vob.setAttribute("file", getThemeFile(themeName, videomode + '_' + "blank.mpg"))
3202 menupgc.appendChild(vob)
3203
3204 titles = dvddom.createElement("titles")
3205
3206 # set the right aspect ratio
3207 title_video = dvddom.createElement("video")
3208 title_video.setAttribute("format", videomode)
3209
3210 # use aspect ratio of video
3211 if getAspectRatioOfVideo(itemNum) > aspectRatioThreshold:
3212 title_video.setAttribute("aspect", "16:9")
3213 title_video.setAttribute("widescreen", "nopanscan")
3214 else:
3215 title_video.setAttribute("aspect", "4:3")
3216
3217 titles.appendChild(title_video)
3218
3219 pgc = dvddom.createElement("pgc")
3220
3221 vob = dvddom.createElement("vob")
3222 vob.setAttribute("file", os.path.join(getItemTempPath(itemNum), "final.vob"))
3223 vob.setAttribute("chapters", createVideoChaptersFixedLength(itemNum,
3224 chapterLength,
3225 getLengthOfVideo(itemNum)))
3226 pgc.appendChild(vob)
3227
3228 del vob
3229 del menupgc
3230
3231 post = dvddom.createElement("post")
3232 if itemNum == numberofitems:
3233 post.appendChild(dvddom.createTextNode("exit;"))
3234 else:
3235 post.appendChild(dvddom.createTextNode("g1=%d;call vmgm menu 2;" % (itemNum + 1)))
3236
3237 pgc.appendChild(post)
3238
3239 titles.appendChild(pgc)
3240 titleset.appendChild(titles)
3241
3242 del pgc
3243 del titles
3244 del title_video
3245 del post
3246 del titleset
3247
3248 itemNum +=1
3249
3250 #Save xml to file
3251 WriteXMLToFile (dvddom,os.path.join(getTempPath(),"dvdauthor.xml"))
3252
3253 #Destroy the DOM and free memory
3254 dvddom.unlink()
3255
3256
3258
3260 previewfolder = os.path.join(getItemTempPath(videoitem), "preview")
3261 if os.path.exists(previewfolder):
3262 deleteAllFilesInFolder(previewfolder)
3263 os.rmdir (previewfolder)
3264 os.makedirs(previewfolder)
3265 return previewfolder
3266
3267
3269
3270def generateVideoPreview(videoitem, itemonthispage, menuitem, starttime, menulength, previewfolder):
3271 """generate thumbnails for a preview in a menu"""
3272
3273 positionx = 9999
3274 positiony = 9999
3275 width = 0
3276 height = 0
3277 maskpicture = None
3278
3279 #run through the theme items and find any graphics that is using a movie identifier
3280 for node in menuitem.childNodes:
3281 if node.nodeName=="graphic":
3282 if node.attributes["filename"].value == "%movie":
3283 #This is a movie preview item so we need to generate the thumbnails
3284 inputfile = os.path.join(getItemTempPath(videoitem),"stream.mv2")
3285 outputfile = os.path.join(previewfolder, "preview-i%d-t%%1-f%%2.jpg" % itemonthispage)
3286 width = getScaledAttribute(node, "w")
3287 height = getScaledAttribute(node, "h")
3288 frames = int(secondsToFrames(menulength))
3289
3290 command = "mytharchivehelper -q -q --createthumbnail --infile %s --thumblist '%s' --outfile %s --framecount %d" % (quoteCmdArg(inputfile), starttime, quoteCmdArg(outputfile), frames)
3291 result = runCommand(command)
3292 if (result != 0):
3293 write( "mytharchivehelper failed with code %d. Command = %s" % (result, command) )
3294
3295 positionx = getScaledAttribute(node, "x")
3296 positiony = getScaledAttribute(node, "y")
3297
3298 #see if this graphics item has a mask
3299 if node.hasAttribute("mask"):
3300 imagemaskfilename = getThemeFile(themeName, node.attributes["mask"].value)
3301 if node.attributes["mask"].value != "" and doesFileExist(imagemaskfilename):
3302 maskpicture = Image.open(imagemaskfilename,"r").resize((width, height))
3303 maskpicture = maskpicture.convert("RGBA")
3304
3305 return (positionx, positiony, width, height, maskpicture)
3306
3307
3309
3310def drawThemeItem(page, itemsonthispage, itemnum, menuitem, bgimage, draw,
3311 bgimagemask, drawmask, highlightcolor, spumuxdom, spunode,
3312 numberofitems, chapternumber, chapterlist):
3313 """Draws text and graphics onto a dvd menu, called by
3314 createMenu and createChapterMenu"""
3315
3316 #Get the XML containing information about this item
3317 infoDOM = xml.dom.minidom.parse(os.path.join(getItemTempPath(itemnum), "info.xml"))
3318
3319 #Error out if its the wrong XML
3320 if infoDOM.documentElement.tagName != "fileinfo":
3321 fatalError("The info.xml file (%s) doesn't look right" %
3322 os.path.join(getItemTempPath(itemnum),"info.xml"))
3323
3324 #boundarybox holds the max and min dimensions for this item
3325 #so we can auto build a menu highlight box
3326 boundarybox = 9999,9999,0,0
3327 wantHighlightBox = True
3328
3329 #Loop through all the nodes inside this menu item
3330 for node in menuitem.childNodes:
3331
3332 #Process each type of item to add it onto the background image
3333 if node.nodeName=="graphic":
3334 #Overlay graphic image onto background
3335
3336 # draw background if required
3337 paintBackground(bgimage, node)
3338
3339 # if this graphic item is a movie thumbnail then we dont process it here
3340 if node.attributes["filename"].value == "%movie":
3341 # this is a movie item but we must still update the boundary box
3342 boundarybox = checkBoundaryBox(boundarybox, node)
3343 else:
3344 imagefilename = expandItemText(infoDOM,
3345 node.attributes["filename"].value,
3346 itemnum, page, itemsonthispage,
3347 chapternumber, chapterlist)
3348
3349 if doesFileExist(imagefilename) == False:
3350 if imagefilename == node.attributes["filename"].value:
3351 imagefilename = getThemeFile(themeName,
3352 node.attributes["filename"].value)
3353
3354 # see if an image mask exists
3355 maskfilename = None
3356 if node.hasAttribute("mask") and node.attributes["mask"].value != "":
3357 maskfilename = getThemeFile(themeName, node.attributes["mask"].value)
3358
3359 # if this is a thumb image and is a MythVideo coverart image then preserve
3360 # its aspect ratio unless overriden later by the theme
3361 if (node.attributes["filename"].value == "%thumbnail"
3362 and getText(infoDOM.getElementsByTagName("coverfile")[0]) !=""):
3363 stretch = False
3364 else:
3365 stretch = True
3366
3367 if paintImage(imagefilename, maskfilename, node, bgimage, stretch):
3368 boundarybox = checkBoundaryBox(boundarybox, node)
3369 else:
3370 write("Image file does not exist '%s'" % imagefilename)
3371
3372 elif node.nodeName == "text":
3373 # Apply some text to the background, including wordwrap if required.
3374
3375 # draw background if required
3376 paintBackground(bgimage, node)
3377
3378 text = expandItemText(infoDOM,node.attributes["value"].value,
3379 itemnum, page, itemsonthispage,
3380 chapternumber, chapterlist)
3381
3382 if text>"":
3383 paintText(draw, bgimage, text, node)
3384
3385 boundarybox = checkBoundaryBox(boundarybox, node)
3386 del text
3387
3388 elif node.nodeName=="previous":
3389 if page>1:
3390 #Overlay previous graphic button onto background
3391
3392 # draw background if required
3393 paintBackground(bgimage, node)
3394
3395 paintButton(draw, bgimage, bgimagemask, node, infoDOM,
3396 itemnum, page, itemsonthispage, chapternumber,
3397 chapterlist)
3398
3399 button = spumuxdom.createElement("button")
3400 button.setAttribute("name","previous")
3401 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
3402 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
3403 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") +
3404 getScaledAttribute(node, "w")))
3405 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
3406 getScaledAttribute(node, "h")))
3407 spunode.appendChild(button)
3408
3409 write( "Added previous page button")
3410
3411
3412 elif node.nodeName == "next":
3413 if itemnum < numberofitems:
3414 #Overlay next graphic button onto background
3415
3416 # draw background if required
3417 paintBackground(bgimage, node)
3418
3419 paintButton(draw, bgimage, bgimagemask, node, infoDOM,
3420 itemnum, page, itemsonthispage, chapternumber,
3421 chapterlist)
3422
3423 button = spumuxdom.createElement("button")
3424 button.setAttribute("name","next")
3425 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
3426 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
3427 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") +
3428 getScaledAttribute(node, "w")))
3429 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
3430 getScaledAttribute(node, "h")))
3431 spunode.appendChild(button)
3432
3433 write("Added next page button")
3434
3435 elif node.nodeName=="playall":
3436 #Overlay playall graphic button onto background
3437
3438 # draw background if required
3439 paintBackground(bgimage, node)
3440
3441 paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
3442 itemsonthispage, chapternumber, chapterlist)
3443
3444 button = spumuxdom.createElement("button")
3445 button.setAttribute("name","playall")
3446 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
3447 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
3448 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") +
3449 getScaledAttribute(node, "w")))
3450 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
3451 getScaledAttribute(node, "h")))
3452 spunode.appendChild(button)
3453
3454 write("Added playall button")
3455
3456 elif node.nodeName == "titlemenu":
3457 if itemnum < numberofitems:
3458 #Overlay next graphic button onto background
3459
3460 # draw background if required
3461 paintBackground(bgimage, node)
3462
3463 paintButton(draw, bgimage, bgimagemask, node, infoDOM,
3464 itemnum, page, itemsonthispage, chapternumber,
3465 chapterlist)
3466
3467 button = spumuxdom.createElement("button")
3468 button.setAttribute("name","titlemenu")
3469 button.setAttribute("x0","%s" % getScaledAttribute(node, "x"))
3470 button.setAttribute("y0","%s" % getScaledAttribute(node, "y"))
3471 button.setAttribute("x1","%s" % (getScaledAttribute(node, "x") +
3472 getScaledAttribute(node, "w")))
3473 button.setAttribute("y1","%s" % (getScaledAttribute(node, "y") +
3474 getScaledAttribute(node, "h")))
3475 spunode.appendChild(button)
3476
3477 write( "Added titlemenu button")
3478
3479 elif node.nodeName=="button":
3480 #Overlay item graphic/text button onto background
3481
3482 # draw background if required
3483 paintBackground(bgimage, node)
3484
3485 wantHighlightBox = False
3486
3487 paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page,
3488 itemsonthispage, chapternumber, chapterlist)
3489
3490 boundarybox = checkBoundaryBox(boundarybox, node)
3491
3492
3493 elif node.nodeName=="#text" or node.nodeName=="#comment":
3494 #Do nothing
3495 assert True
3496 else:
3497 write( "Dont know how to process %s" % node.nodeName)
3498
3499 if drawmask is None:
3500 return
3501
3502 #Draw the selection mask for this item
3503 if wantHighlightBox == True:
3504 # Make the boundary box bigger than the content to avoid over writing it
3505 boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
3506 drawmask.rectangle(boundarybox,outline=highlightcolor)
3507
3508 # Draw another line to make the box thicker - PIL does not support linewidth
3509 boundarybox=boundarybox[0]-1,boundarybox[1]-1,boundarybox[2]+1,boundarybox[3]+1
3510 drawmask.rectangle(boundarybox,outline=highlightcolor)
3511
3512 node = spumuxdom.createElement("button")
3513 #Fiddle this for chapter marks....
3514 if chapternumber>0:
3515 node.setAttribute("name","%s" % chapternumber)
3516 else:
3517 node.setAttribute("name","%s" % itemnum)
3518 node.setAttribute("x0","%d" % int(boundarybox[0]))
3519 node.setAttribute("y0","%d" % int(boundarybox[1]))
3520 node.setAttribute("x1","%d" % int(boundarybox[2] + 1))
3521 node.setAttribute("y1","%d" % int(boundarybox[3] + 1))
3522 spunode.appendChild(node)
3523
3524
3526
3527def createMenu(screensize, screendpi, numberofitems):
3528 """Creates all the necessary menu images and files for the MythBurn menus."""
3529
3530 #Get the main menu node (we must only have 1)
3531 menunode=themeDOM.getElementsByTagName("menu")
3532 if menunode.length!=1:
3533 fatalError("Cannot find menu element in theme file")
3534 menunode=menunode[0]
3535
3536 menuitems=menunode.getElementsByTagName("item")
3537 #Total number of video items on a single menu page (no less than 1!)
3538 itemsperpage = menuitems.length
3539 write( "Menu items per page %s" % itemsperpage)
3540
3541 #Get background image filename
3542 backgroundfilename = menunode.attributes["background"].value
3543 if backgroundfilename=="":
3544 fatalError("Background image is not set in theme file")
3545
3546 backgroundfilename = getThemeFile(themeName,backgroundfilename)
3547 write( "Background image file is %s" % backgroundfilename)
3548 if not doesFileExist(backgroundfilename):
3549 fatalError("Background image not found (%s)" % backgroundfilename)
3550
3551 #Get highlight color
3552 highlightcolor = "red"
3553 if menunode.hasAttribute("highlightcolor"):
3554 highlightcolor = menunode.attributes["highlightcolor"].value
3555
3556 #Get menu music
3557 menumusic = "menumusic.ac3"
3558 if menunode.hasAttribute("music"):
3559 menumusic = menunode.attributes["music"].value
3560
3561 #Get menu length
3562 menulength = 15
3563 if menunode.hasAttribute("length"):
3564 menulength = int(menunode.attributes["length"].value)
3565
3566 write("Music is %s, length is %s seconds" % (menumusic, menulength))
3567
3568 #Page number counter
3569 page=1
3570
3571 #Item counter to indicate current video item
3572 itemnum=1
3573
3574 write("Creating DVD menus")
3575
3576 while itemnum <= numberofitems:
3577 write("Menu page %s" % page)
3578
3579 #need to check if any of the videos are flaged as movies
3580 #and if so generate the required preview
3581
3582 write("Creating Preview Video")
3583 previewitem = itemnum
3584 itemsonthispage = 0
3585 haspreview = False
3586
3587 previewx = []
3588 previewy = []
3589 previeww = []
3590 previewh = []
3591 previewmask = []
3592
3593 while previewitem <= numberofitems and itemsonthispage < itemsperpage:
3594 menuitem=menuitems[ itemsonthispage ]
3595 itemsonthispage+=1
3596
3597 #make sure the preview folder is empty and present
3598 previewfolder = createEmptyPreviewFolder(previewitem)
3599
3600 #and then generate the preview if required (px=9999 means not required)
3601 px, py, pw, ph, maskimage = generateVideoPreview(previewitem, itemsonthispage, menuitem, 0, menulength, previewfolder)
3602 previewx.append(px)
3603 previewy.append(py)
3604 previeww.append(pw)
3605 previewh.append(ph)
3606 previewmask.append(maskimage)
3607 if px != 9999:
3608 haspreview = True
3609
3610 previewitem+=1
3611
3612 #previews generated but need to save where we started from
3613 savedpreviewitem = itemnum
3614
3615 #Number of video items on this menu page
3616 itemsonthispage=0
3617
3618 #instead of loading the background image and drawing on it we now
3619 #make a transparent image and draw all items on it. This overlay
3620 #image is then added to the required background image when the
3621 #preview items are added (the reason for this is it will assist
3622 #if the background image is actually a video)
3623
3624 overlayimage=Image.new("RGBA",screensize)
3625 draw=ImageDraw.Draw(overlayimage)
3626
3627 #Create image to hold button masks (same size as background)
3628 bgimagemask=Image.new("RGBA",overlayimage.size)
3629 drawmask=ImageDraw.Draw(bgimagemask)
3630
3631 spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
3632 spunode = spumuxdom.documentElement.firstChild.firstChild
3633
3634 #Loop through all the items on this menu page
3635 while itemnum <= numberofitems and itemsonthispage < itemsperpage:
3636 menuitem=menuitems[ itemsonthispage ]
3637
3638 itemsonthispage+=1
3639
3640 drawThemeItem(page, itemsonthispage,
3641 itemnum, menuitem, overlayimage,
3642 draw, bgimagemask, drawmask, highlightcolor,
3643 spumuxdom, spunode, numberofitems, 0,"")
3644
3645 #On to the next item
3646 itemnum+=1
3647
3648 #Paste the overlay image onto the background
3649 bgimage=Image.open(backgroundfilename,"r").resize(screensize)
3650 bgimage.paste(overlayimage, (0,0), overlayimage)
3651
3652 #Save this menu image and its mask
3653 rgb_bgimage=bgimage.convert('RGB')
3654 rgb_bgimage.save(os.path.join(getTempPath(),"background-%s.jpg" % page),"JPEG", quality=99)
3655 del rgb_bgimage
3656 bgimagemask.save(os.path.join(getTempPath(),"backgroundmask-%s.png" % page),"PNG",quality=99,optimize=0,dpi=screendpi)
3657
3658 #now that the base background has been made and all the previews generated
3659 #we need to add the previews to the background
3660 #Assumption: We assume that there is nothing in the location of where the items go
3661 #(ie, no text on the images)
3662
3663 itemsonthispage = 0
3664
3665 #numframes should be the number of preview images that have been created
3666 numframes=secondsToFrames(menulength)
3667
3668 # only generate the preview video if required.
3669 if haspreview == True:
3670 write( "Generating the preview images" )
3671 framenum = 0
3672 while framenum < numframes:
3673 previewitem = savedpreviewitem
3674 itemsonthispage = 0
3675 while previewitem <= numberofitems and itemsonthispage < itemsperpage:
3676 itemsonthispage+=1
3677 if previewx[itemsonthispage-1] != 9999:
3678 previewpath = os.path.join(getItemTempPath(previewitem), "preview")
3679 previewfile = "preview-i%d-t1-f%d.jpg" % (itemsonthispage, framenum)
3680 imagefile = os.path.join(previewpath, previewfile)
3681
3682 if doesFileExist(imagefile):
3683 picture = Image.open(imagefile, "r").resize((previeww[itemsonthispage-1], previewh[itemsonthispage-1]))
3684 picture = picture.convert("RGBA")
3685 imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % itemsonthispage)
3686 if previewmask[itemsonthispage-1] is not None:
3687 bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1]), previewmask[itemsonthispage-1])
3688 else:
3689 bgimage.paste(picture, (previewx[itemsonthispage-1], previewy[itemsonthispage-1]))
3690 del picture
3691 previewitem+=1
3692 #bgimage.save(os.path.join(getTempPath(),"background-%s-f%06d.png" % (page, framenum)),"PNG",quality=100,optimize=0,dpi=screendpi)
3693 rgb_bgimage=bgimage.convert('RGB')
3694 rgb_bgimage.save(os.path.join(getTempPath(),"background-%s-f%06d.jpg" % (page, framenum)),"JPEG",quality=99)
3695 del rgb_bgimage
3696 framenum+=1
3697
3698 spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
3699 spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"backgroundmask-%s.png" % page))
3700
3701 #Release large amounts of memory ASAP !
3702 del draw
3703 del bgimage
3704 del drawmask
3705 del bgimagemask
3706 del overlayimage
3707 del previewx
3708 del previewy
3709 del previewmask
3710
3711 WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"spumux-%s.xml" % page))
3712
3713 if mainmenuAspectRatio == "4:3":
3714 aspect_ratio = 2
3715 else:
3716 aspect_ratio = 3
3717
3718 write("Encoding Menu Page %s using aspect ratio '%s'" % (page, mainmenuAspectRatio))
3719 if haspreview == True:
3720 encodeMenu(os.path.join(getTempPath(),"background-%s-f%%06d.jpg" % page),
3721 os.path.join(getTempPath(),"temp.m2v"),
3722 getThemeFile(themeName,menumusic),
3723 menulength,
3724 os.path.join(getTempPath(),"temp.mpg"),
3725 os.path.join(getTempPath(),"spumux-%s.xml" % page),
3726 os.path.join(getTempPath(),"menu-%s.mpg" % page),
3727 aspect_ratio)
3728 else:
3729 encodeMenu(os.path.join(getTempPath(),"background-%s.jpg" % page),
3730 os.path.join(getTempPath(),"temp.m2v"),
3731 getThemeFile(themeName,menumusic),
3732 menulength,
3733 os.path.join(getTempPath(),"temp.mpg"),
3734 os.path.join(getTempPath(),"spumux-%s.xml" % page),
3735 os.path.join(getTempPath(),"menu-%s.mpg" % page),
3736 aspect_ratio)
3737
3738 #Move on to the next page
3739 page+=1
3740
3741
3743
3744def createChapterMenu(screensize, screendpi, numberofitems):
3745 """Creates all the necessary menu images and files for the MythBurn menus."""
3746
3747 #Get the main menu node (we must only have 1)
3748 menunode=themeDOM.getElementsByTagName("submenu")
3749 if menunode.length!=1:
3750 fatalError("Cannot find submenu element in theme file")
3751 menunode=menunode[0]
3752
3753 menuitems=menunode.getElementsByTagName("chapter")
3754 #Total number of video items on a single menu page (no less than 1!)
3755 itemsperpage = menuitems.length
3756 write( "Chapter items per page %s " % itemsperpage)
3757
3758 #Get background image filename
3759 backgroundfilename = menunode.attributes["background"].value
3760 if backgroundfilename=="":
3761 fatalError("Background image is not set in theme file")
3762 backgroundfilename = getThemeFile(themeName,backgroundfilename)
3763 write( "Background image file is %s" % backgroundfilename)
3764 if not doesFileExist(backgroundfilename):
3765 fatalError("Background image not found (%s)" % backgroundfilename)
3766
3767 #Get highlight color
3768 highlightcolor = "red"
3769 if menunode.hasAttribute("highlightcolor"):
3770 highlightcolor = menunode.attributes["highlightcolor"].value
3771
3772 #Get menu music
3773 menumusic = "menumusic.ac3"
3774 if menunode.hasAttribute("music"):
3775 menumusic = menunode.attributes["music"].value
3776
3777 #Get menu length
3778 menulength = 15
3779 if menunode.hasAttribute("length"):
3780 menulength = int(menunode.attributes["length"].value)
3781
3782 write("Music is %s, length is %s seconds" % (menumusic, menulength))
3783
3784 #Page number counter
3785 page=1
3786
3787 write( "Creating DVD sub-menus")
3788
3789 while page <= numberofitems:
3790 write( "Sub-menu %s " % page)
3791
3792 #instead of loading the background image and drawing on it we now
3793 #make a transparent image and draw all items on it. This overlay
3794 #image is then added to the required background image when the
3795 #preview items are added (the reason for this is it will assist
3796 #if the background image is actually a video)
3797
3798 overlayimage=Image.new("RGBA",screensize, (0,0,0,0))
3799 draw=ImageDraw.Draw(overlayimage)
3800
3801 #Create image to hold button masks (same size as background)
3802 bgimagemask=Image.new("RGBA",overlayimage.size, (0,0,0,0))
3803 drawmask=ImageDraw.Draw(bgimagemask)
3804
3805 spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
3806 spunode = spumuxdom.documentElement.firstChild.firstChild
3807
3808 #Extract the thumbnails
3809 chapterlist=createVideoChapters(page,itemsperpage,getLengthOfVideo(page),True)
3810 chapterlist=chapterlist.split(",")
3811
3812 #now need to preprocess the menu to see if any preview videos are required
3813 #This must be done on an individual basis since we do the resize as the
3814 #images are extracted.
3815
3816 #first make sure the preview folder is empty and present
3817 previewfolder = createEmptyPreviewFolder(page)
3818
3819 haspreview = False
3820
3821 previewtime = 0
3822 previewchapter = 0
3823 previewx = []
3824 previewy = []
3825 previeww = []
3826 previewh = []
3827 previewmask = []
3828
3829 while previewchapter < itemsperpage:
3830 menuitem=menuitems[ previewchapter ]
3831
3832 previewtime = timeStringToSeconds(chapterlist[previewchapter])
3833
3834 #generate the preview if required (px=9999 means not required)
3835 px, py, pw, ph, maskimage = generateVideoPreview(page, previewchapter, menuitem, previewtime, menulength, previewfolder)
3836 previewx.append(px)
3837 previewy.append(py)
3838 previeww.append(pw)
3839 previewh.append(ph)
3840 previewmask.append(maskimage)
3841
3842 if px != 9999:
3843 haspreview = True
3844
3845 previewchapter+=1
3846
3847 #Loop through all the items on this menu page
3848 chapter=0
3849 while chapter < itemsperpage: # and itemsonthispage < itemsperpage:
3850 menuitem=menuitems[ chapter ]
3851 chapter+=1
3852
3853 drawThemeItem(page, itemsperpage, page, menuitem,
3854 overlayimage, draw,
3855 bgimagemask, drawmask, highlightcolor,
3856 spumuxdom, spunode,
3857 999, chapter, chapterlist)
3858
3859 #Save this menu image and its mask
3860 bgimage=Image.open(backgroundfilename,"r").resize(screensize)
3861 bgimage.paste(overlayimage, (0,0), overlayimage)
3862 rgb_bgimage=bgimage.convert('RGB')
3863 rgb_bgimage.save(os.path.join(getTempPath(),"chaptermenu-%s.jpg" % page),"JPEG", quality=99)
3864 del rgb_bgimage
3865
3866 bgimagemask.save(os.path.join(getTempPath(),"chaptermenumask-%s.png" % page),"PNG",quality=90,optimize=0)
3867
3868 if haspreview == True:
3869 numframes=secondsToFrames(menulength)
3870
3871 #numframes should be the number of preview images that have been created
3872
3873 write( "Generating the preview images" )
3874 framenum = 0
3875 while framenum < numframes:
3876 previewchapter = 0
3877 while previewchapter < itemsperpage:
3878 if previewx[previewchapter] != 9999:
3879 previewpath = os.path.join(getItemTempPath(page), "preview")
3880 previewfile = "preview-i%d-t1-f%d.jpg" % (previewchapter, framenum)
3881 imagefile = os.path.join(previewpath, previewfile)
3882
3883 if doesFileExist(imagefile):
3884 picture = Image.open(imagefile, "r").resize((previeww[previewchapter], previewh[previewchapter]))
3885 picture = picture.convert("RGBA")
3886 imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % previewchapter)
3887 if previewmask[previewchapter] is not None:
3888 bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter]), previewmask[previewchapter])
3889 else:
3890 bgimage.paste(picture, (previewx[previewchapter], previewy[previewchapter]))
3891 del picture
3892 previewchapter+=1
3893 rgb_bgimage=bgimage.convert('RGB')
3894 rgb_bgimage.save(os.path.join(getTempPath(),"chaptermenu-%s-f%06d.jpg" % (page, framenum)),"JPEG",quality=99)
3895 del rgb_bgimage
3896 framenum+=1
3897
3898 spumuxdom.documentElement.firstChild.firstChild.setAttribute("select",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
3899 spumuxdom.documentElement.firstChild.firstChild.setAttribute("highlight",os.path.join(getTempPath(),"chaptermenumask-%s.png" % page))
3900
3901 #Release large amounts of memory ASAP !
3902 del draw
3903 del bgimage
3904 del drawmask
3905 del bgimagemask
3906 del overlayimage
3907 del previewx
3908 del previewy
3909 del previewmask
3910
3911 #write( spumuxdom.toprettyxml())
3912 WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"chapterspumux-%s.xml" % page))
3913
3914 if chaptermenuAspectRatio == "4:3":
3915 aspect_ratio = '2'
3916 elif chaptermenuAspectRatio == "16:9":
3917 aspect_ratio = '3'
3918 else:
3919 if getAspectRatioOfVideo(page) > aspectRatioThreshold:
3920 aspect_ratio = '3'
3921 else:
3922 aspect_ratio = '2'
3923
3924 write("Encoding Chapter Menu Page %s using aspect ratio '%s'" % (page, chaptermenuAspectRatio))
3925
3926 if haspreview == True:
3927 encodeMenu(os.path.join(getTempPath(),"chaptermenu-%s-f%%06d.jpg" % page),
3928 os.path.join(getTempPath(),"temp.m2v"),
3929 getThemeFile(themeName,menumusic),
3930 menulength,
3931 os.path.join(getTempPath(),"temp.mpg"),
3932 os.path.join(getTempPath(),"chapterspumux-%s.xml" % page),
3933 os.path.join(getTempPath(),"chaptermenu-%s.mpg" % page),
3934 aspect_ratio)
3935 else:
3936 encodeMenu(os.path.join(getTempPath(),"chaptermenu-%s.jpg" % page),
3937 os.path.join(getTempPath(),"temp.m2v"),
3938 getThemeFile(themeName,menumusic),
3939 menulength,
3940 os.path.join(getTempPath(),"temp.mpg"),
3941 os.path.join(getTempPath(),"chapterspumux-%s.xml" % page),
3942 os.path.join(getTempPath(),"chaptermenu-%s.mpg" % page),
3943 aspect_ratio)
3944
3945 #Move on to the next page
3946 page+=1
3947
3948
3950
3951def createDetailsPage(screensize, screendpi, numberofitems):
3952 """Creates all the necessary images and files for the details page."""
3953
3954 write( "Creating details pages")
3955
3956 #Get the detailspage node (we must only have 1)
3957 detailnode=themeDOM.getElementsByTagName("detailspage")
3958 if detailnode.length!=1:
3959 fatalError("Cannot find detailspage element in theme file")
3960 detailnode=detailnode[0]
3961
3962 #Get background image filename
3963 backgroundfilename = detailnode.attributes["background"].value
3964 if backgroundfilename=="":
3965 fatalError("Background image is not set in theme file")
3966 backgroundfilename = getThemeFile(themeName,backgroundfilename)
3967 write( "Background image file is %s" % backgroundfilename)
3968 if not doesFileExist(backgroundfilename):
3969 fatalError("Background image not found (%s)" % backgroundfilename)
3970
3971 #Get menu music
3972 menumusic = "menumusic.ac3"
3973 if detailnode.hasAttribute("music"):
3974 menumusic = detailnode.attributes["music"].value
3975
3976 #Get menu length
3977 menulength = 15
3978 if detailnode.hasAttribute("length"):
3979 menulength = int(detailnode.attributes["length"].value)
3980
3981 write("Music is %s, length is %s seconds" % (menumusic, menulength))
3982
3983 #Item counter to indicate current video item
3984 itemnum=1
3985
3986 while itemnum <= numberofitems:
3987 write( "Creating details page for %s" % itemnum)
3988
3989 #make sure the preview folder is empty and present
3990 previewfolder = createEmptyPreviewFolder(itemnum)
3991 haspreview = False
3992
3993 #and then generate the preview if required (px=9999 means not required)
3994 previewx, previewy, previeww, previewh, previewmask = generateVideoPreview(itemnum, 1, detailnode, 0, menulength, previewfolder)
3995 if previewx != 9999:
3996 haspreview = True
3997
3998 #instead of loading the background image and drawing on it we now
3999 #make a transparent image and draw all items on it. This overlay
4000 #image is then added to the required background image when the
4001 #preview items are added (the reason for this is it will assist
4002 #if the background image is actually a video)
4003
4004 overlayimage=Image.new("RGBA",screensize, (0,0,0,0))
4005 draw=ImageDraw.Draw(overlayimage)
4006
4007 spumuxdom = xml.dom.minidom.parseString('<subpictures><stream><spu force="yes" start="00:00:00.0" highlight="" select="" ></spu></stream></subpictures>')
4008 spunode = spumuxdom.documentElement.firstChild.firstChild
4009
4010 drawThemeItem(0, 0, itemnum, detailnode, overlayimage, draw, None, None,
4011 "", spumuxdom, spunode, numberofitems, 0, "")
4012
4013 #Save this details image
4014 bgimage=Image.open(backgroundfilename,"r").resize(screensize)
4015 bgimage.paste(overlayimage, (0,0), overlayimage)
4016 rgb_bgimage=bgimage.convert('RGB')
4017 rgb_bgimage.save(os.path.join(getTempPath(),"details-%s.jpg" % itemnum),"JPEG", quality=99)
4018 del rgb_bgimage
4019
4020 if haspreview == True:
4021 numframes=secondsToFrames(menulength)
4022
4023 #numframes should be the number of preview images that have been created
4024 write( "Generating the detail preview images" )
4025 framenum = 0
4026 while framenum < numframes:
4027 if previewx != 9999:
4028 previewpath = os.path.join(getItemTempPath(itemnum), "preview")
4029 previewfile = "preview-i%d-t1-f%d.jpg" % (1, framenum)
4030 imagefile = os.path.join(previewpath, previewfile)
4031
4032 if doesFileExist(imagefile):
4033 picture = Image.open(imagefile, "r").resize((previeww, previewh))
4034 picture = picture.convert("RGBA")
4035 imagemaskfile = os.path.join(previewpath, "mask-i%d.png" % 1)
4036 if previewmask is not None:
4037 bgimage.paste(picture, (previewx, previewy), previewmask)
4038 else:
4039 bgimage.paste(picture, (previewx, previewy))
4040 del picture
4041 rgb_bgimage=bgimage.convert('RGB')
4042 rgb_bgimage.save(os.path.join(getTempPath(),"details-%s-f%06d.jpg" % (itemnum, framenum)),"JPEG",quality=99)
4043 del rgb_bgimage
4044 framenum+=1
4045
4046
4047 #Release large amounts of memory ASAP !
4048 del draw
4049 del bgimage
4050
4051 # always use the same aspect ratio as the video
4052 aspect_ratio='2'
4053 if getAspectRatioOfVideo(itemnum) > aspectRatioThreshold:
4054 aspect_ratio='3'
4055
4056 #write( spumuxdom.toprettyxml())
4057 WriteXMLToFile (spumuxdom,os.path.join(getTempPath(),"detailsspumux-%s.xml" % itemnum))
4058
4059 write("Encoding Details Page %s" % itemnum)
4060 if haspreview == True:
4061 encodeMenu(os.path.join(getTempPath(),"details-%s-f%%06d.jpg" % itemnum),
4062 os.path.join(getTempPath(),"temp.m2v"),
4063 getThemeFile(themeName,menumusic),
4064 menulength,
4065 os.path.join(getTempPath(),"temp.mpg"),
4066 "",
4067 os.path.join(getTempPath(),"details-%s.mpg" % itemnum),
4068 aspect_ratio)
4069 else:
4070 encodeMenu(os.path.join(getTempPath(),"details-%s.jpg" % itemnum),
4071 os.path.join(getTempPath(),"temp.m2v"),
4072 getThemeFile(themeName,menumusic),
4073 menulength,
4074 os.path.join(getTempPath(),"temp.mpg"),
4075 "",
4076 os.path.join(getTempPath(),"details-%s.mpg" % itemnum),
4077 aspect_ratio)
4078
4079 #On to the next item
4080 itemnum+=1
4081
4082
4084
4086 fh = open(file, 'rb')
4087 Magic = fh.read(4)
4088 fh.close()
4089 return Magic=="RIFF"
4090
4091
4093
4094def processAudio(folder):
4095 """encode audio to ac3 for better compression and compatability with NTSC players"""
4096
4097 # process track 1
4098 if not encodetoac3 and doesFileExist(os.path.join(folder,'stream0.mp2')):
4099 #don't re-encode to ac3 if the user doesn't want it
4100 write( "Audio track 1 is in mp2 format - NOT re-encoding to ac3")
4101 elif doesFileExist(os.path.join(folder,'stream0.mp2'))==True:
4102 write( "Audio track 1 is in mp2 format - re-encoding to ac3")
4103 encodeAudio("ac3",os.path.join(folder,'stream0.mp2'), os.path.join(folder,'stream0.ac3'),True)
4104 elif doesFileExist(os.path.join(folder,'stream0.mpa'))==True:
4105 write( "Audio track 1 is in mpa format - re-encoding to ac3")
4106 encodeAudio("ac3",os.path.join(folder,'stream0.mpa'), os.path.join(folder,'stream0.ac3'),True)
4107 elif doesFileExist(os.path.join(folder,'stream0.ac3'))==True:
4108 write( "Audio is already in ac3 format")
4109 else:
4110 fatalError("Track 1 - Unknown audio format or de-multiplex failed!")
4111
4112 # process track 2
4113 if not encodetoac3 and doesFileExist(os.path.join(folder,'stream1.mp2')):
4114 #don't re-encode to ac3 if the user doesn't want it
4115 write( "Audio track 2 is in mp2 format - NOT re-encoding to ac3")
4116 elif doesFileExist(os.path.join(folder,'stream1.mp2'))==True:
4117 write( "Audio track 2 is in mp2 format - re-encoding to ac3")
4118 encodeAudio("ac3",os.path.join(folder,'stream1.mp2'), os.path.join(folder,'stream1.ac3'),True)
4119 elif doesFileExist(os.path.join(folder,'stream1.mpa'))==True:
4120 write( "Audio track 2 is in mpa format - re-encoding to ac3")
4121 encodeAudio("ac3",os.path.join(folder,'stream1.mpa'), os.path.join(folder,'stream1.ac3'),True)
4122 elif doesFileExist(os.path.join(folder,'stream1.ac3'))==True:
4123 write( "Audio is already in ac3 format")
4124
4125
4127
4128# tuple index constants
4129VIDEO_INDEX = 0
4130VIDEO_CODEC = 1
4131VIDEO_ID = 2
4132
4133AUDIO_INDEX = 0
4134AUDIO_CODEC = 1
4135AUDIO_ID = 2
4136AUDIO_LANG = 3
4137
4138def selectStreams(folder):
4139 """Choose the streams we want from the source file"""
4140
4141 video = (-1, 'N/A', -1) # index, codec, ID
4142 audio1 = (-1, 'N/A', -1, 'N/A') # index, codec, ID, lang
4143 audio2 = (-1, 'N/A', -1, 'N/A')
4144
4145 #open the XML containing information about this file
4146 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
4147 #error out if its the wrong XML
4148 if infoDOM.documentElement.tagName != "file":
4149 fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
4150
4151
4152 #get video ID, CODEC
4153 nodes = infoDOM.getElementsByTagName("video")
4154 if nodes.length == 0:
4155 write("Didn't find any video elements in stream info file.!!!")
4156 write("");
4157 sys.exit(1)
4158 if nodes.length > 1:
4159 write("Found more than one video element in stream info file.!!!")
4160 node = nodes[0]
4161 video = (int(node.attributes["ffmpegindex"].value), node.attributes["codec"].value, int(node.attributes["id"].value))
4162
4163 #get audioID's - we choose the best 2 audio streams using this algorithm
4164 # 1. if there is one or more stream(s) using the 1st preferred language we use that
4165 # 2. if there is one or more stream(s) using the 2nd preferred language we use that
4166 # 3. if we still haven't found a stream we use the stream with the lowest PID
4167 # 4. we prefer ac3 over mp2
4168 # 5. if there are more than one stream with the chosen language we use the one with the lowest PID
4169
4170 write("Preferred audio languages %s and %s" % (preferredlang1, preferredlang2))
4171
4172 nodes = infoDOM.getElementsByTagName("audio")
4173
4174 if nodes.length == 0:
4175 write("Didn't find any audio elements in stream info file.!!!")
4176 write("");
4177 sys.exit(1)
4178
4179 found = False
4180 # first try to find a stream with ac3 and preferred language 1
4181 for node in nodes:
4182 index = int(node.attributes["ffmpegindex"].value)
4183 lang = node.attributes["language"].value
4184 format = node.attributes["codec"].value.upper()
4185 pid = int(node.attributes["id"].value)
4186 if lang == preferredlang1 and format == "AC3":
4187 if found:
4188 if pid < audio1[AUDIO_ID]:
4189 audio1 = (index, format, pid, lang)
4190 else:
4191 audio1 = (index, format, pid, lang)
4192 found = True
4193
4194 # second try to find a stream with mp2 and preferred language 1
4195 if not found:
4196 for node in nodes:
4197 index = int(node.attributes["ffmpegindex"].value)
4198 lang = node.attributes["language"].value
4199 format = node.attributes["codec"].value.upper()
4200 pid = int(node.attributes["id"].value)
4201 if lang == preferredlang1 and format == "MP2":
4202 if found:
4203 if pid < audio1[AUDIO_ID]:
4204 audio1 = (index, format, pid, lang)
4205 else:
4206 audio1 = (index, format, pid, lang)
4207 found = True
4208
4209 # finally use the stream with the lowest pid, prefer ac3 over mp2
4210 if not found:
4211 for node in nodes:
4212 index = int(node.attributes["ffmpegindex"].value)
4213 format = node.attributes["codec"].value.upper()
4214 pid = int(node.attributes["id"].value)
4215 if not found:
4216 audio1 = (index, format, pid, lang)
4217 found = True
4218 else:
4219 if format == "AC3" and audio1[AUDIO_CODEC] == "MP2":
4220 audio1 = (index, format, pid, lang)
4221 else:
4222 if pid < audio1[AUDIO_ID]:
4223 audio1 = (index, format, pid, lang)
4224
4225 # do we need to find a second audio stream?
4226 if preferredlang1 != preferredlang2 and nodes.length > 1:
4227 found = False
4228 # first try to find a stream with ac3 and preferred language 2
4229 for node in nodes:
4230 index = int(node.attributes["ffmpegindex"].value)
4231 lang = node.attributes["language"].value
4232 format = node.attributes["codec"].value.upper()
4233 pid = int(node.attributes["id"].value)
4234 if lang == preferredlang2 and format == "AC3":
4235 if found:
4236 if pid < audio2[AUDIO_ID]:
4237 audio2 = (index, format, pid, lang)
4238 else:
4239 audio2 = (index, format, pid, lang)
4240 found = True
4241
4242 # second try to find a stream with mp2 and preferred language 2
4243 if not found:
4244 for node in nodes:
4245 index = int(node.attributes["ffmpegindex"].value)
4246 lang = node.attributes["language"].value
4247 format = node.attributes["codec"].value.upper()
4248 pid = int(node.attributes["id"].value)
4249 if lang == preferredlang2 and format == "MP2":
4250 if found:
4251 if pid < audio2[AUDIO_ID]:
4252 audio2 = (index, format, pid, lang)
4253 else:
4254 audio2 = (index, format, pid, lang)
4255 found = True
4256
4257 # finally use the stream with the lowest pid, prefer ac3 over mp2
4258 if not found:
4259 for node in nodes:
4260 index = int(node.attributes["ffmpegindex"].value)
4261 format = node.attributes["codec"].value.upper()
4262 pid = int(node.attributes["id"].value)
4263 if not found:
4264 # make sure we don't choose the same stream as audio1
4265 if pid != audio1[AUDIO_ID]:
4266 audio2 = (index, format, pid, lang)
4267 found = True
4268 else:
4269 if format == "AC3" and audio2[AUDIO_CODEC] == "MP2" and pid != audio1[AUDIO_ID]:
4270 audio2 = (index, format, pid, lang)
4271 else:
4272 if pid < audio2[AUDIO_ID] and pid != audio1[AUDIO_ID]:
4273 audio2 = (index, format, pid, lang)
4274
4275 write("Video id: 0x%x, Audio1: [%d] 0x%x (%s, %s), Audio2: [%d] - 0x%x (%s, %s)" % \
4276 (video[VIDEO_ID], audio1[AUDIO_INDEX], audio1[AUDIO_ID], audio1[AUDIO_CODEC], audio1[AUDIO_LANG], \
4277 audio2[AUDIO_INDEX], audio2[AUDIO_ID], audio2[AUDIO_CODEC], audio2[AUDIO_LANG]))
4278
4279 return (video, audio1, audio2)
4280
4281
4283
4284# tuple index constants
4285SUBTITLE_INDEX = 0
4286SUBTITLE_CODEC = 1
4287SUBTITLE_ID = 2
4288SUBTITLE_LANG = 3
4289
4291 """Choose the subtitle stream we want from the source file"""
4292
4293 subtitle = (-1, 'N/A', -1, 'N/A') # index, codec, ID, lang
4294
4295 #open the XML containing information about this file
4296 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
4297 #error out if its the wrong XML
4298 if infoDOM.documentElement.tagName != "file":
4299 fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
4300
4301
4302 #get subtitle nodes
4303 nodes = infoDOM.getElementsByTagName("subtitle")
4304 if nodes.length == 0:
4305 write("Didn't find any subtitle elements in stream info file.")
4306 return subtitle
4307
4308 write("Preferred languages %s and %s" % (preferredlang1, preferredlang2))
4309
4310 found = False
4311 # first try to find a stream with preferred language 1
4312 for node in nodes:
4313 index = int(node.attributes["ffmpegindex"].value)
4314 lang = node.attributes["language"].value
4315 format = node.attributes["codec"].value.upper()
4316 pid = int(node.attributes["id"].value)
4317 if not found and lang == preferredlang1 and format == "dvbsub":
4318 subtitle = (index, format, pid, lang)
4319 found = True
4320
4321 # second try to find a stream with preferred language 2
4322 if not found:
4323 for node in nodes:
4324 index = int(node.attributes["ffmpegindex"].value)
4325 lang = node.attributes["language"].value
4326 format = node.attributes["codec"].value.upper()
4327 pid = int(node.attributes["id"].value)
4328 if not found and lang == preferredlang2 and format == "dvbsub":
4329 subtitle = (index, format, pid, lang)
4330 found = True
4331
4332 # finally use the first subtitle stream
4333 if not found:
4334 for node in nodes:
4335 index = int(node.attributes["ffmpegindex"].value)
4336 format = node.attributes["codec"].value.upper()
4337 pid = int(node.attributes["id"].value)
4338 if not found:
4339 subtitle = (index, format, pid, lang)
4340 found = True
4341
4342 write("Subtitle id: 0x%x" % (subtitle[SUBTITLE_ID]))
4343
4344 return subtitle
4345
4346
4348
4350 """figure out what aspect ratio we want from the source file"""
4351
4352 #this should be smarter and look though the file for any AR changes
4353 #at the moment it just uses the AR found at the start of the file
4354
4355 #open the XML containing information about this file
4356 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
4357 #error out if its the wrong XML
4358 if infoDOM.documentElement.tagName != "file":
4359 fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
4360
4361
4362 #get aspect ratio
4363 nodes = infoDOM.getElementsByTagName("video")
4364 if nodes.length == 0:
4365 write("Didn't find any video elements in stream info file.!!!")
4366 write("");
4367 sys.exit(1)
4368 if nodes.length > 1:
4369 write("Found more than one video element in stream info file.!!!")
4370 node = nodes[0]
4371 try:
4372 ar = float(node.attributes["aspectratio"].value)
4373 if ar > float(4.0/3.0 - 0.01) and ar < float(4.0/3.0 + 0.01):
4374 aspectratio = "4:3"
4375 write("Aspect ratio is 4:3")
4376 elif ar > float(16.0/9.0 - 0.01) and ar < float(16.0/9.0 + 0.01):
4377 aspectratio = "16:9"
4378 write("Aspect ratio is 16:9")
4379 else:
4380 write("Unknown aspect ratio %f - Using 16:9" % ar)
4381 aspectratio = "16:9"
4382 except:
4383 aspectratio = "16:9"
4384
4385 return aspectratio
4386
4387
4389
4390def getVideoCodec(folder):
4391 """Get the video codec from the streaminfo.xml for the file"""
4392
4393 #open the XML containing information about this file
4394 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
4395 #error out if its the wrong XML
4396 if infoDOM.documentElement.tagName != "file":
4397 fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
4398
4399 nodes = infoDOM.getElementsByTagName("video")
4400 if nodes.length == 0:
4401 write("Didn't find any video elements in stream info file!!!")
4402 write("");
4403 sys.exit(1)
4404 if nodes.length > 1:
4405 write("Found more than one video element in stream info file!!!")
4406 node = nodes[0]
4407 return node.attributes["codec"].value
4408
4409
4411
4412def getFileType(folder):
4413 """Get the overall file type from the streaminfo.xml for the file"""
4414
4415 #open the XML containing information about this file
4416 infoDOM = xml.dom.minidom.parse(os.path.join(folder, 'streaminfo.xml'))
4417 #error out if its the wrong XML
4418 if infoDOM.documentElement.tagName != "file":
4419 fatalError("This does not look like a stream info file (%s)" % os.path.join(folder, 'streaminfo.xml'))
4420
4421 nodes = infoDOM.getElementsByTagName("file")
4422 if nodes.length == 0:
4423 write("Didn't find any file elements in stream info file!!!")
4424 write("");
4425 sys.exit(1)
4426 if nodes.length > 1:
4427 write("Found more than one file element in stream info file!!!")
4428 node = nodes[0]
4429
4430 return node.attributes["type"].value
4431
4432
4434
4435def getStreamList(folder):
4436
4437 # choose which streams we need
4438 video, audio1, audio2 = selectStreams(folder)
4439
4440 streamList = "0x%x" % video[VIDEO_ID]
4441
4442 if audio1[AUDIO_ID] != -1:
4443 streamList += ",0x%x" % audio1[AUDIO_ID]
4444
4445 if audio2[AUDIO_ID] != -1:
4446 streamList += ",0x%x" % audio2[AUDIO_ID]
4447
4448 # add subtitle stream id if required
4449 if addSubtitles:
4450 subtitles = selectSubtitleStream(folder)
4451 if subtitles[SUBTITLE_ID] != -1:
4452 streamList += ",0x%x" % subtitles[SUBTITLE_ID]
4453
4454 return streamList;
4455
4456
4457
4459
4460def isFileOkayForDVD(file, folder):
4461 """return true if the file is dvd compliant"""
4462
4463 if not getVideoCodec(folder).lower().startswith("mpeg2video"):
4464 return False
4465
4466
4467# if (getAudioCodec(folder)).lower() != "ac3" and encodeToAC3:
4468# return False
4469
4470 videosize = getVideoSize(os.path.join(folder, "streaminfo.xml"))
4471
4472 # has the user elected to re-encode the file
4473 if file.hasAttribute("encodingprofile"):
4474 if file.attributes["encodingprofile"].value != "NONE":
4475 write("File will be re-encoded using profile %s" % file.attributes["encodingprofile"].value)
4476 return False
4477
4478 if not isResolutionOkayForDVD(videosize):
4479 # file does not have a dvd resolution
4480 if file.hasAttribute("encodingprofile"):
4481 if file.attributes["encodingprofile"].value == "NONE":
4482 write("WARNING: File does not have a DVD compliant resolution but "
4483 "you have selected not to re-encode the file")
4484 return True
4485 else:
4486 return False
4487
4488 return True
4489
4490
4493
4494def processFile(file, folder, count):
4495 """Process a single video/recording file ready for burning."""
4496
4497 if useprojectx:
4498 doProcessFileProjectX(file, folder, count)
4499 else:
4500 doProcessFile(file, folder, count)
4501
4502
4505
4506def doProcessFile(file, folder, count):
4507 """Process a single video/recording file ready for burning."""
4508
4509 write( "*************************************************************")
4510 write( "Processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
4511 write( "*************************************************************")
4512
4513 #As part of this routine we need to pre-process the video this MAY mean:
4514 #1. removing commercials/cleaning up mpeg2 stream
4515 #2. encoding to mpeg2 (if its an avi for instance or isn't DVD compatible)
4516 #3. selecting audio track to use and encoding audio from mp2 into ac3
4517 #4. de-multiplexing into video and audio steams)
4518
4519 mediafile=""
4520
4521 if file.hasAttribute("localfilename"):
4522 mediafile=file.attributes["localfilename"].value
4523 elif file.attributes["type"].value=="recording":
4524 mediafile = file.attributes["filename"].value
4525 elif file.attributes["type"].value=="video":
4526 mediafile=os.path.join(videopath, file.attributes["filename"].value)
4527 elif file.attributes["type"].value=="file":
4528 mediafile=file.attributes["filename"].value
4529 else:
4530 fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
4531
4532 #Get the XML containing information about this item
4533 infoDOM = xml.dom.minidom.parse( os.path.join(folder,"info.xml") )
4534 #Error out if its the wrong XML
4535 if infoDOM.documentElement.tagName != "fileinfo":
4536 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
4537
4538 #If this is an mpeg2 myth recording and there is a cut list available and the user wants to use it
4539 #run mythtranscode to cut out commercials etc
4540 if file.attributes["type"].value == "recording":
4541 #can only use mythtranscode to cut commercials on mpeg2 files
4542 write("File type is '%s'" % getFileType(folder))
4543 write("Video codec is '%s'" % getVideoCodec(folder))
4544 if (getVideoCodec(folder)).lower().startswith("mpeg2video"):
4545 if file.attributes["usecutlist"].value == "1" and getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes":
4546 # Run from local file?
4547 if file.hasAttribute("localfilename"):
4548 localfile = file.attributes["localfilename"].value
4549 else:
4550 localfile = ""
4551 write("File has a cut list - running mythtranscode to remove unwanted segments")
4552 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4553 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4554 if runMythtranscode(chanid, starttime, os.path.join(folder,'newfile.mpg'), True, localfile):
4555 mediafile = os.path.join(folder,'newfile.mpg')
4556 else:
4557 write("Failed to run mythtranscode to remove unwanted segments")
4558 else:
4559 #does the user always want to run recordings through mythtranscode?
4560 #may help to fix any errors in the file
4561 if (alwaysRunMythtranscode == True or
4562 (getFileType(folder) == "mpegts" and isFileOkayForDVD(file, folder))):
4563 # Run from local file?
4564 if file.hasAttribute("localfilename"):
4565 localfile = file.attributes["localfilename"].value
4566 else:
4567 localfile = ""
4568 write("Running mythtranscode --mpeg2 to fix any errors")
4569 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4570 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4571 if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
4572 mediafile = os.path.join(folder, 'newfile.mpg')
4573 else:
4574 write("Failed to run mythtranscode to fix any errors")
4575 else:
4576 #does the user always want to run mpeg2 files through mythtranscode?
4577 #may help to fix any errors in the file
4578 write("File type is '%s'" % getFileType(folder))
4579 write("Video codec is '%s'" % getVideoCodec(folder))
4580
4581 if (alwaysRunMythtranscode == True and
4582 getVideoCodec(folder).lower().startswith("mpeg2video") and
4583 isFileOkayForDVD(file, folder)):
4584 if file.hasAttribute("localfilename"):
4585 localfile = file.attributes["localfilename"].value
4586 else:
4587 localfile = file.attributes["filename"].value
4588 write("Running mythtranscode --mpeg2 to fix any errors")
4589 chanid = -1
4590 starttime = -1
4591 if runMythtranscode(chanid, starttime, os.path.join(folder, 'newfile.mpg'), False, localfile):
4592 mediafile = os.path.join(folder, 'newfile.mpg')
4593 else:
4594 write("Failed to run mythtranscode to fix any errors")
4595
4596 #do we need to re-encode the file to make it DVD compliant?
4597 if not isFileOkayForDVD(file, folder):
4598 if getFileType(folder) == 'nuv':
4599 #file is a nuv file which mythffmpeg has problems reading so use mythtranscode to pass
4600 #the video and audio streams to mythffmpeg to do the reencode
4601
4602 #we need to re-encode the file, make sure we get the right video/audio streams
4603 #would be good if we could also split the file at the same time
4604 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
4605
4606 #choose which streams we need
4607 video, audio1, audio2 = selectStreams(folder)
4608
4609 #choose which aspect ratio we should use
4610 aspectratio = selectAspectRatio(folder)
4611
4612 write("Re-encoding audio and video from nuv file")
4613
4614 # what encoding profile should we use
4615 if file.hasAttribute("encodingprofile"):
4616 profile = file.attributes["encodingprofile"].value
4617 else:
4618 profile = defaultEncodingProfile
4619
4620 if file.hasAttribute("localfilename"):
4621 mediafile = file.attributes["localfilename"].value
4622 chanid = -1
4623 starttime = -1
4624 usecutlist = -1
4625 elif file.attributes["type"].value == "recording":
4626 mediafile = -1
4627 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4628 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4629 usecutlist = (file.attributes["usecutlist"].value == "1" and
4630 getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes")
4631 else:
4632 chanid = -1
4633 starttime = -1
4634 usecutlist = -1
4635
4636 encodeNuvToMPEG2(chanid, starttime, mediafile, os.path.join(folder, "newfile2.mpg"), folder,
4637 profile, usecutlist)
4638 mediafile = os.path.join(folder, 'newfile2.mpg')
4639 else:
4640 #we need to re-encode the file, make sure we get the right video/audio streams
4641 #would be good if we could also split the file at the same time
4642 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
4643
4644 #choose which streams we need
4645 video, audio1, audio2 = selectStreams(folder)
4646
4647 #choose which aspect ratio we should use
4648 aspectratio = selectAspectRatio(folder)
4649
4650 write("Re-encoding audio and video")
4651
4652 # Run from local file?
4653 if file.hasAttribute("localfilename"):
4654 mediafile = file.attributes["localfilename"].value
4655
4656 # what encoding profile should we use
4657 if file.hasAttribute("encodingprofile"):
4658 profile = file.attributes["encodingprofile"].value
4659 else:
4660 profile = defaultEncodingProfile
4661
4662 #do the re-encode
4663 encodeVideoToMPEG2(mediafile, os.path.join(folder, "newfile2.mpg"), video,
4664 audio1, audio2, aspectratio, profile)
4665 mediafile = os.path.join(folder, 'newfile2.mpg')
4666
4667 #remove the old mediafile that was run through mythtranscode
4668 #if it exists
4669 if debug_keeptempfiles==False:
4670 if os.path.exists(os.path.join(folder, "newfile.mpg")):
4671 os.remove(os.path.join(folder,'newfile.mpg'))
4672
4673 # the file is now DVD compliant split it into video and audio parts
4674
4675 # find out what streams we have available now
4676 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 1)
4677
4678 # choose which streams we need
4679 video, audio1, audio2 = selectStreams(folder)
4680
4681 # now attempt to split the source file into video and audio parts
4682 write("Splitting MPEG stream into audio and video parts")
4683 deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2)
4684
4685 # remove intermediate files
4686 if debug_keeptempfiles==False:
4687 if os.path.exists(os.path.join(folder, "newfile.mpg")):
4688 os.remove(os.path.join(folder,'newfile.mpg'))
4689 if os.path.exists(os.path.join(folder, "newfile2.mpg")):
4690 os.remove(os.path.join(folder,'newfile2.mpg'))
4691
4692 # we now have a video stream and one or more audio streams
4693 # check if we need to convert any of the audio streams to ac3
4694 processAudio(folder)
4695
4696 # if we don't already have one find a title thumbnail image
4697 titleImage = os.path.join(folder, "title.jpg")
4698 if not os.path.exists(titleImage):
4699 # if the file is a recording try to use its preview image for the thumb
4700 if file.attributes["type"].value == "recording":
4701 previewImage = file.attributes["filename"].value + ".png"
4702 if usebookmark == True and os.path.exists(previewImage):
4703 copy(previewImage, titleImage)
4704 else:
4705 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
4706 else:
4707 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
4708
4709 write( "*************************************************************")
4710 write( "Finished processing '%s'" % file.attributes["filename"].value)
4711 write( "*************************************************************")
4712
4713
4714
4717
4718def doProcessFileProjectX(file, folder, count):
4719 """Process a single video/recording file ready for burning."""
4720
4721 write( "*************************************************************")
4722 write( "Processing %s %d: '%s'" % (file.attributes["type"].value, count, file.attributes["filename"].value))
4723 write( "*************************************************************")
4724
4725 #As part of this routine we need to pre-process the video this MAY mean:
4726 #1. encoding to mpeg2 (if its an avi for instance or isn't DVD compatible)
4727 #2. removing commercials/cleaning up mpeg2 stream
4728 #3. selecting audio track(s) to use and encoding audio from mp2 into ac3
4729 #4. de-multiplexing into video and audio steams
4730
4731 mediafile=""
4732
4733 if file.hasAttribute("localfilename"):
4734 mediafile=file.attributes["localfilename"].value
4735 elif file.attributes["type"].value=="recording":
4736 mediafile = file.attributes["filename"].value
4737 elif file.attributes["type"].value=="video":
4738 mediafile=os.path.join(videopath, file.attributes["filename"].value)
4739 elif file.attributes["type"].value=="file":
4740 mediafile=file.attributes["filename"].value
4741 else:
4742 fatalError("Unknown type of video file it must be 'recording', 'video' or 'file'.")
4743
4744 #Get the XML containing information about this item
4745 infoDOM = xml.dom.minidom.parse( os.path.join(folder,"info.xml") )
4746 #Error out if its the wrong XML
4747 if infoDOM.documentElement.tagName != "fileinfo":
4748 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
4749
4750 #do we need to re-encode the file to make it DVD compliant?
4751 if not isFileOkayForDVD(file, folder):
4752 if getFileType(folder) == 'nuv':
4753 #file is a nuv file which mythffmpeg has problems reading so use mythtranscode to pass
4754 #the video and audio streams to mythffmpeg to do the reencode
4755
4756 #we need to re-encode the file, make sure we get the right video/audio streams
4757 #would be good if we could also split the file at the same time
4758 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
4759
4760 #choose which streams we need
4761 video, audio1, audio2 = selectStreams(folder)
4762
4763 #choose which aspect ratio we should use
4764 aspectratio = selectAspectRatio(folder)
4765
4766 write("Re-encoding audio and video from nuv file")
4767
4768 # what encoding profile should we use
4769 if file.hasAttribute("encodingprofile"):
4770 profile = file.attributes["encodingprofile"].value
4771 else:
4772 profile = defaultEncodingProfile
4773
4774 if file.hasAttribute("localfilename"):
4775 mediafile = file.attributes["localfilename"].value
4776 chanid = -1
4777 starttime = -1
4778 usecutlist = -1
4779 elif file.attributes["type"].value == "recording":
4780 mediafile = -1
4781 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4782 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4783 usecutlist = (file.attributes["usecutlist"].value == "1" and
4784 getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes")
4785 else:
4786 chanid = -1
4787 starttime = -1
4788 usecutlist = -1
4789
4790 encodeNuvToMPEG2(chanid, starttime, mediafile, os.path.join(folder, "newfile2.mpg"), folder,
4791 profile, usecutlist)
4792 mediafile = os.path.join(folder, 'newfile2.mpg')
4793 else:
4794 #we need to re-encode the file, make sure we get the right video/audio streams
4795 #would be good if we could also split the file at the same time
4796 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 0)
4797
4798 #choose which streams we need
4799 video, audio1, audio2 = selectStreams(folder)
4800
4801 #choose which aspect ratio we should use
4802 aspectratio = selectAspectRatio(folder)
4803
4804 write("Re-encoding audio and video")
4805
4806 # Run from local file?
4807 if file.hasAttribute("localfilename"):
4808 mediafile = file.attributes["localfilename"].value
4809
4810 # what encoding profile should we use
4811 if file.hasAttribute("encodingprofile"):
4812 profile = file.attributes["encodingprofile"].value
4813 else:
4814 profile = defaultEncodingProfile
4815
4816 #do the re-encode
4817 encodeVideoToMPEG2(mediafile, os.path.join(folder, "newfile2.mpg"), video,
4818 audio1, audio2, aspectratio, profile)
4819 mediafile = os.path.join(folder, 'newfile2.mpg')
4820
4821 #remove an intermediate file
4822 if os.path.exists(os.path.join(folder, "newfile1.mpg")):
4823 os.remove(os.path.join(folder,'newfile1.mpg'))
4824
4825 # the file is now DVD compliant now we need to remove commercials
4826 # and split it into video, audio, subtitle parts
4827
4828 # find out what streams we have available now
4829 getStreamInformation(mediafile, os.path.join(folder, "streaminfo.xml"), 1)
4830
4831 # choose which streams we need
4832 video, audio1, audio2 = selectStreams(folder)
4833
4834 # now attempt to split the source file into video and audio parts
4835 # using projectX
4836
4837 # If this is an mpeg2 myth recording and there is a cut list available and the
4838 # user wants to use it run projectx to cut out commercials etc
4839 if file.attributes["type"].value == "recording":
4840 if file.attributes["usecutlist"].value == "1" and getText(infoDOM.getElementsByTagName("hascutlist")[0]) == "yes":
4841 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4842 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4843 write("File has a cut list - running Project-X to remove unwanted segments")
4844 if not runProjectX(chanid, starttime, folder, True, mediafile):
4845 fatalError("Failed to run Project-X to remove unwanted segments and demux")
4846 else:
4847 # no cutlist so just demux this file
4848 chanid = getText(infoDOM.getElementsByTagName("chanid")[0])
4849 starttime = getText(infoDOM.getElementsByTagName("starttime")[0])
4850 write("Using Project-X to demux file")
4851 if not runProjectX(chanid, starttime, folder, False, mediafile):
4852 fatalError("Failed to run Project-X to demux file")
4853 else:
4854 # just demux this file
4855 chanid = -1
4856 starttime = -1
4857 write("Running Project-X to demux file")
4858 if not runProjectX(chanid, starttime, folder, False, mediafile):
4859 fatalError("Failed to run Project-X to demux file")
4860
4861 # we now have a video stream and one or more audio streams
4862 # check if we need to convert any of the audio streams to ac3
4863 processAudio(folder)
4864
4865 # if we don't already have one find a title thumbnail image
4866 titleImage = os.path.join(folder, "title.jpg")
4867 if not os.path.exists(titleImage):
4868 # if the file is a recording try to use its preview image for the thumb
4869 if file.attributes["type"].value == "recording":
4870 previewImage = file.attributes["filename"].value + ".png"
4871 if usebookmark == True and os.path.exists(previewImage):
4872 copy(previewImage, titleImage)
4873 else:
4874 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
4875 else:
4876 extractVideoFrame(os.path.join(folder, "stream.mv2"), titleImage, thumboffset)
4877
4878 write( "*************************************************************")
4879 write( "Finished processing file '%s'" % file.attributes["filename"].value)
4880 write( "*************************************************************")
4881
4882
4884
4885def copyRemote(files, tmpPath):
4886 '''go through the list of files looking for files on remote filesytems
4887 and copy them to a local file for quicker processing'''
4888 localTmpPath = os.path.join(tmpPath, "localcopy")
4889 for node in files:
4890 tmpfile = node.attributes["filename"].value
4891 filename = os.path.basename(tmpfile)
4892
4893 res = runCommand("mytharchivehelper -q -q --isremote --infile " + quoteCmdArg(tmpfile))
4894 #If User wants to, copy remote files to a tmp dir
4895 if res == 2 and copyremoteFiles==True:
4896 # file is on a remote filesystem so copy it to a local file
4897 write("Copying file from " + tmpfile)
4898 write("to " + os.path.join(localTmpPath, filename))
4899
4900 # Copy file
4901 if not doesFileExist(os.path.join(localTmpPath, filename)):
4902 copy(tmpfile, os.path.join(localTmpPath, filename))
4903
4904 # update node
4905 node.setAttribute("localfilename", os.path.join(localTmpPath, filename))
4906 elif res == 3:
4907 # file is on a remote myth backend so copy it to a local file
4908 write("Copying file from " + tmpfile)
4909 localfile = os.path.join(localTmpPath, filename)
4910 write("to " + localfile)
4911
4912 # Copy file
4913 if not doesFileExist(localfile):
4914 runCommand("mythutil --copyfile --infile " + quoteCmdArg(tmpfile) + " --outfile " + quoteCmdArg(localfile))
4915
4916 # update node
4917 node.setAttribute("localfilename", localfile)
4918 return files
4919
4920
4922
4923def processJob(job):
4924 """Starts processing a MythBurn job, expects XML nodes to be passed as input."""
4925 global wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage
4926 global themeDOM, themeName, themeFonts
4927
4928
4929 media=job.getElementsByTagName("media")
4930
4931 if media.length==1:
4932
4933 themeName=job.attributes["theme"].value
4934
4935 #Check theme exists
4936 if not validateTheme(themeName):
4937 fatalError("Failed to validate theme (%s)" % themeName)
4938 #Get the theme XML
4939 themeDOM = getThemeConfigurationXML(themeName)
4940
4941 #Pre generate all the fonts we need
4942 loadFonts(themeDOM)
4943
4944 #Update the global flags
4945 nodes=themeDOM.getElementsByTagName("intro")
4946 wantIntro = (nodes.length > 0)
4947
4948 nodes=themeDOM.getElementsByTagName("menu")
4949 wantMainMenu = (nodes.length > 0)
4950
4951 nodes=themeDOM.getElementsByTagName("submenu")
4952 wantChapterMenu = (nodes.length > 0)
4953
4954 nodes=themeDOM.getElementsByTagName("detailspage")
4955 wantDetailsPage = (nodes.length > 0)
4956
4957 write( "wantIntro: %d, wantMainMenu: %d, wantChapterMenu: %d, wantDetailsPage: %d" \
4958 % (wantIntro, wantMainMenu, wantChapterMenu, wantDetailsPage))
4959
4960 if videomode=="ntsc":
4961 format=dvdNTSC
4962 dpi=dvdNTSCdpi
4963 elif videomode=="pal":
4964 format=dvdPAL
4965 dpi=dvdPALdpi
4966 else:
4967 fatalError("Unknown videomode is set (%s)" % videomode)
4968
4969 write( "Final DVD Video format will be " + videomode)
4970
4971
4972 #Loop through all the files
4973 files=media[0].getElementsByTagName("file")
4974 filecount=0
4975 if files.length > 0:
4976 write( "There are %s file(s) to process" % files.length)
4977
4978 if debug_secondrunthrough==False:
4979 #Delete all the temporary files that currently exist
4981 localCopyFolder=os.path.join(getTempPath(),"localcopy")
4982 os.makedirs(localCopyFolder)
4983 files=copyRemote(files,getTempPath())
4984
4985 #First pass through the files to be recorded - sense check
4986 #we dont want to find half way through this long process that
4987 #a file does not exist, or is the wrong format!!
4988 for node in files:
4989 filecount+=1
4990
4991 #Generate a temp folder name for this file
4992 folder=getItemTempPath(filecount)
4993
4994 if debug_secondrunthrough==False:
4995 os.makedirs(folder)
4996 #Do the pre-process work
4997 preProcessFile(node,folder,filecount)
4998
4999 if debug_secondrunthrough==False:
5000 #Loop through all the files again but this time do more serious work!
5001 filecount=0
5002 for node in files:
5003 filecount+=1
5004 folder=getItemTempPath(filecount)
5005
5006 #Process this file
5007 processFile(node,folder,filecount)
5008
5009 #We can only create the menus after the videos have been processed
5010 #and the commercials cut out so we get the correct run time length
5011 #for the chapter marks and thumbnails.
5012 #create the DVD menus...
5013 if wantMainMenu:
5014 createMenu(format, dpi, files.length)
5015
5016 #Submenus are visible when you select the chapter menu while the recording is playing
5017 if wantChapterMenu:
5018 createChapterMenu(format, dpi, files.length)
5019
5020 #Details Page are displayed just before playing each recording
5021 if wantDetailsPage:
5022 createDetailsPage(format, dpi, files.length)
5023
5024 #DVD Author file
5025 if not wantMainMenu and not wantChapterMenu:
5026 createDVDAuthorXMLNoMenus(format, files.length)
5027 elif not wantMainMenu:
5028 createDVDAuthorXMLNoMainMenu(format, files.length)
5029 else:
5030 createDVDAuthorXML(format, files.length)
5031
5032 #Check all the files will fit onto a recordable DVD
5033 if mediatype == DVD_DL:
5034 # dual layer
5035 performMPEG2Shrink(files, dvdrsize[1])
5036 else:
5037 #single layer
5038 performMPEG2Shrink(files, dvdrsize[0])
5039
5040 filecount=0
5041 for node in files:
5042 filecount+=1
5043 folder=getItemTempPath(filecount)
5044 #Multiplex this file
5045 #(This also removes non-required audio feeds inside mpeg streams
5046 #(through re-multiplexing) we only take 1 video and 1 or 2 audio streams)
5047 pid=multiplexMPEGStream(os.path.join(folder,'stream.mv2'),
5048 os.path.join(folder,'stream0'),
5049 os.path.join(folder,'stream1'),
5050 os.path.join(folder,'final.vob'),
5051 calcSyncOffset(filecount))
5052
5053 #Now all the files are completed and ready to be burnt
5054 runDVDAuthor()
5055
5056 #Delete dvdauthor work files
5057 if debug_keeptempfiles==False:
5058 filecount=0
5059 for node in files:
5060 filecount+=1
5061 folder=getItemTempPath(filecount)
5062 if os.path.exists(os.path.join(folder, "stream.mv2")):
5063 os.remove(os.path.join(folder,'stream.mv2'))
5064 if os.path.exists(os.path.join(folder, "stream0.mp2")):
5065 os.remove(os.path.join(folder,'stream0.mp2'))
5066 if os.path.exists(os.path.join(folder, "stream1.mp2")):
5067 os.remove(os.path.join(folder,'stream1.mp2'))
5068 if os.path.exists(os.path.join(folder, "stream0.ac3")):
5069 os.remove(os.path.join(folder,'stream0.ac3'))
5070 if os.path.exists(os.path.join(folder, "stream1.ac3")):
5071 os.remove(os.path.join(folder,'stream1.ac3'))
5072
5073 #Get DVD title from first processed file
5074 #Get the XML containing information about this item
5075 infoDOM = xml.dom.minidom.parse( os.path.join(getItemTempPath(1),"info.xml") )
5076 #Error out if its the wrong XML
5077 if infoDOM.documentElement.tagName != "fileinfo":
5078 fatalError("The info.xml file (%s) doesn't look right" % os.path.join(folder,"info.xml"))
5079 title = expandItemText(infoDOM,"%title",1,0,0,0,0)
5080 # replace all non-ascii-characters
5081 title = title.encode('ascii', 'replace').decode('ascii', 'replace')
5082 title = title.strip()
5083
5084 # replace not-allowed characters
5085 index = 0
5086 title_new = ''
5087 while (index < len(title)) and (index < 32):
5088 if title[index].isalnum and title[index] != ' ':
5089 title_new += title[index]
5090 else:
5091 title_new += '_'
5092 index = index + 1
5093
5094 title = title_new.upper()
5095
5096 if len(title) < 1:
5097 title = 'UNNAMED'
5098
5099 #Create the DVD ISO image
5100 if docreateiso == True or mediatype == FILE:
5101 CreateDVDISO(title)
5102
5103 #Burn the DVD ISO image
5104 if doburn == True and mediatype != FILE:
5105 BurnDVDISO(title)
5106
5107 #Move the created iso image to the given location
5108 if mediatype == FILE and savefilename != "":
5109 write("Moving ISO image to: %s" % savefilename)
5110 try:
5111 os.rename(os.path.join(getTempPath(), 'mythburn.iso'), savefilename)
5112 except:
5113 f1 = open(os.path.join(getTempPath(), 'mythburn.iso'), 'rb')
5114 f2 = open(savefilename, 'wb')
5115 data = f1.read(1024 * 1024)
5116 while data:
5117 f2.write(data)
5118 data = f1.read(1024 * 1024)
5119 f1.close()
5120 f2.close()
5121 os.unlink(os.path.join(getTempPath(), 'mythburn.iso'))
5122 else:
5123 write( "Nothing to do! (files)")
5124 else:
5125 write( "Nothing to do! (media)")
5126 return
5127
5128
5130
5131def usage():
5132 write("""
5133 -h/--help (Show this usage)
5134 -j/--jobfile file (use file as the job file)
5135 -l/--progresslog file (log file to output progress messages)
5136
5137 """)
5138
5139#############################################################
5140# The main starting point for mythburn.py
5141
5142def main():
5143 global sharepath, scriptpath, cpuCount, videopath, gallerypath, musicpath
5144 global videomode, temppath, logpath, dvddrivepath, dbVersion, preferredlang1
5145 global preferredlang2, useFIFO, encodetoac3, alwaysRunMythtranscode
5146 global copyremoteFiles, mainmenuAspectRatio, chaptermenuAspectRatio, dateformat
5147 global timeformat, clearArchiveTable, nicelevel, drivespeed, path_mplex
5148 global path_dvdauthor, path_mkisofs, path_growisofs, path_M2VRequantiser, addSubtitles
5149 global path_jpeg2yuv, path_spumux, path_mpeg2enc, path_projectx, useprojectx, progresslog
5150 global progressfile, jobfile
5151
5152 write( "mythburn.py (%s) starting up..." % VERSION)
5153
5154 #Ensure we are running at least python 2.3.5
5155 if not hasattr(sys, "hexversion") or sys.hexversion < 0x20305F0:
5156 sys.stderr.write("Sorry, your Python is too old. Please upgrade at least to 2.3.5\n")
5157 sys.exit(1)
5158
5159 # figure out where this script is located
5160 scriptpath = os.path.dirname(sys.argv[0])
5161 scriptpath = os.path.abspath(scriptpath)
5162 write("script path:" + scriptpath)
5163
5164 # figure out where the myth share directory is located
5165 sharepath = os.path.split(scriptpath)[0]
5166 sharepath = os.path.split(sharepath)[0]
5167 write("myth share path:" + sharepath)
5168
5169 # process any command line options
5170 try:
5171 opts, args = getopt.getopt(sys.argv[1:], "j:hl:", ["jobfile=", "help", "progresslog="])
5172 except getopt.GetoptError:
5173 # print usage and exit
5174 usage()
5175 sys.exit(2)
5176
5177 for o, a in opts:
5178 if o in ("-h", "--help"):
5179 usage()
5180 sys.exit()
5181 if o in ("-j", "--jobfile"):
5182 jobfile = str(a)
5183 write("passed job file: " + a)
5184 if o in ("-l", "--progresslog"):
5185 progresslog = str(a)
5186 write("passed progress log file: " + a)
5187
5188 #if we have been given a progresslog filename to write to open it
5189 if progresslog != "":
5190 if os.path.exists(progresslog):
5191 os.remove(progresslog)
5192 progressfile = open(progresslog, 'w', encoding='utf-8')
5193 write( "mythburn.py (%s) starting up..." % VERSION)
5194
5195 #Get mysql database parameters
5196 #getMysqlDBParameters()
5197
5198 saveSetting("MythArchiveLastRunStart", time.strftime("%Y-%m-%d %H:%M:%S "))
5199 saveSetting("MythArchiveLastRunType", "DVD")
5200 saveSetting("MythArchiveLastRunStatus", "Running")
5201
5202 cpuCount = getCPUCount()
5203
5204 #if the script is run from the web interface the PATH environment variable does not include
5205 #many of the bin locations we need so just append a few likely locations where our required
5206 #executables may be
5207 if not os.environ['PATH'].endswith(':'):
5208 os.environ['PATH'] += ":"
5209 os.environ['PATH'] += "/bin:/sbin:/usr/local/bin:/usr/bin:/opt/bin:" + installPrefix +"/bin:"
5210
5211 #Get defaults from MythTV database
5212 defaultsettings = getDefaultParametersFromMythTVDB()
5213 videopath = defaultsettings.get("VideoStartupDir", None)
5214 gallerypath = defaultsettings.get("GalleryDir", None)
5215 musicpath = defaultsettings.get("MusicLocation", None)
5216 videomode = defaultsettings["MythArchiveVideoFormat"].lower()
5217 temppath = os.path.join(defaultsettings["MythArchiveTempDir"], "work")
5218 logpath = os.path.join(defaultsettings["MythArchiveTempDir"], "logs")
5219 write("temppath: " + temppath)
5220 write("logpath: " + logpath)
5221 dvddrivepath = defaultsettings["MythArchiveDVDLocation"]
5222 dbVersion = defaultsettings["DBSchemaVer"]
5223 preferredlang1 = defaultsettings["ISO639Language0"]
5224 preferredlang2 = defaultsettings["ISO639Language1"]
5225 useFIFO = (defaultsettings["MythArchiveUseFIFO"] == '1')
5226 alwaysRunMythtranscode = (defaultsettings["MythArchiveAlwaysUseMythTranscode"] == '1')
5227 copyremoteFiles = (defaultsettings["MythArchiveCopyRemoteFiles"] == '1')
5228 mainmenuAspectRatio = defaultsettings["MythArchiveMainMenuAR"]
5229 chaptermenuAspectRatio = defaultsettings["MythArchiveChapterMenuAR"]
5230 dateformat = defaultsettings.get("MythArchiveDateFormat", "%a %d %b %Y")
5231 timeformat = defaultsettings.get("MythArchiveTimeFormat", "%I:%M %p")
5232 drivespeed = int(defaultsettings.get("MythArchiveDriveSpeed", "0"))
5233 if "MythArchiveClearArchiveTable" in defaultsettings:
5234 clearArchiveTable = (defaultsettings["MythArchiveClearArchiveTable"] == '1')
5235 nicelevel = defaultsettings.get("JobQueueCPU", "0")
5236
5237 # external commands
5238 path_mplex = [defaultsettings["MythArchiveMplexCmd"], os.path.split(defaultsettings["MythArchiveMplexCmd"])[1]]
5239 path_dvdauthor = [defaultsettings["MythArchiveDvdauthorCmd"], os.path.split(defaultsettings["MythArchiveDvdauthorCmd"])[1]]
5240 path_mkisofs = [defaultsettings["MythArchiveMkisofsCmd"], os.path.split(defaultsettings["MythArchiveMkisofsCmd"])[1]]
5241 path_growisofs = [defaultsettings["MythArchiveGrowisofsCmd"], os.path.split(defaultsettings["MythArchiveGrowisofsCmd"])[1]]
5242 path_M2VRequantiser = [defaultsettings["MythArchiveM2VRequantiserCmd"], os.path.split(defaultsettings["MythArchiveM2VRequantiserCmd"])[1]]
5243 path_jpeg2yuv = [defaultsettings["MythArchiveJpeg2yuvCmd"], os.path.split(defaultsettings["MythArchiveJpeg2yuvCmd"])[1]]
5244 path_spumux = [defaultsettings["MythArchiveSpumuxCmd"], os.path.split(defaultsettings["MythArchiveSpumuxCmd"])[1]]
5245 path_mpeg2enc = [defaultsettings["MythArchiveMpeg2encCmd"], os.path.split(defaultsettings["MythArchiveMpeg2encCmd"])[1]]
5246
5247 path_projectx = [defaultsettings["MythArchiveProjectXCmd"], os.path.split(defaultsettings["MythArchiveProjectXCmd"])[1]]
5248 useprojectx = (defaultsettings["MythArchiveUseProjectX"] == '1')
5249 addSubtitles = (defaultsettings["MythArchiveAddSubtitles"] == '1')
5250
5251 # sanity check
5252 if path_projectx[0] == "":
5253 useprojectx = False
5254
5255 if nicelevel == '1':
5256 nicelevel = 10
5257 elif nicelevel == '2':
5258 nicelevel = 0
5259 else:
5260 nicelevel = 17
5261
5262 nicelevel = os.nice(nicelevel)
5263 write( "Setting process priority to %s" % nicelevel)
5264
5265 try:
5266 import psutil
5267 except ImportError:
5268 write( "Cannot change ionice level")
5269 else:
5270 write( "Setting ionice level to idle")
5271 p = psutil.Process(os.getpid())
5272 p.ionice(psutil.IOPRIO_CLASS_IDLE)
5273
5274 import errno
5275
5276 try:
5277 # Attempt to create a lock file so any UI knows we are running.
5278 # Testing for and creation of the lock is one atomic operation.
5279 lckpath = os.path.join(logpath, "mythburn.lck")
5280 try:
5281 fd = os.open(lckpath, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
5282 try:
5283 os.write(fd, b"%d\n" % os.getpid())
5284 os.close(fd)
5285 except:
5286 os.remove(lckpath)
5287 raise
5288 except OSError as e:
5289 if e.errno == errno.EEXIST:
5290 write("Lock file exists -- already running???")
5291 sys.exit(1)
5292 else:
5293 fatalError("cannot create lockfile: %s" % e)
5294 # if we get here, we own the lock
5295
5296 try:
5297 #Load XML input file from disk
5298 jobDOM = xml.dom.minidom.parse(jobfile)
5299
5300 #Error out if its the wrong XML
5301 if jobDOM.documentElement.tagName != "mythburn":
5302 fatalError("Job file doesn't look right!")
5303
5304 #process each job
5305 jobcount=0
5306 jobs=jobDOM.getElementsByTagName("job")
5307 for job in jobs:
5308 jobcount+=1
5309 write( "Processing Mythburn job number %s." % jobcount)
5310
5311 #get any options from the job file if present
5312 options = job.getElementsByTagName("options")
5313 if options.length > 0:
5314 getOptions(options)
5315
5316 processJob(job)
5317
5318 jobDOM.unlink()
5319
5320 # clear the archiveitems table
5321 if clearArchiveTable == True:
5323
5324 saveSetting("MythArchiveLastRunStatus", "Success")
5325 saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
5326 write("Finished processing jobs!!!")
5327 finally:
5328 # remove our lock file
5329 os.remove(lckpath)
5330
5331 # make sure the files we created are read/writable by all
5332 os.system("chmod -R a+rw-x+X %s" % defaultsettings["MythArchiveTempDir"])
5333 except SystemExit:
5334 write("Terminated")
5335 except:
5336 write('-'*60)
5337 traceback.print_exc(file=sys.stdout)
5338 if progresslog != "":
5339 traceback.print_exc(file=progressfile)
5340 write('-'*60)
5341 saveSetting("MythArchiveLastRunStatus", "Failed")
5342 saveSetting("MythArchiveLastRunEnd", time.strftime("%Y-%m-%d %H:%M:%S "))
5343
5344if __name__ == "__main__":
5345 main()
5346
5347os.putenv("LC_ALL", oldlocale)
static uint32_t getsize(int fd)
Definition: avi.cpp:64
class to hold a font definition
Definition: mythburn.py:258
def __init__(self, name=None, fontFile=None, size=19, color="white", effect="normal", shadowColor="black", shadowSize=1)
Definition: mythburn.py:259
def drawText(self, text, color=None)
Definition: mythburn.py:275
def getFont(self)
Definition: mythburn.py:269
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)
MBASE_PUBLIC long long copy(QFile &dst, QFile &src, uint block_size=0)
Copies src file to dst file.
def createVideoChapters(itemnum, numofchapters, lengthofvideo, getthumbnails)
Creates a set of chapter points evenly spread thoughout a file Optionally grabs the thumbnails from t...
Definition: mythburn.py:787
def createChapterMenu(screensize, screendpi, numberofitems)
creates a chapter menu for a file on a DVD
Definition: mythburn.py:3744
def quoteString(str)
Return the input string with single quotes escaped.
Definition: mythburn.py:346
def isMediaAVIFile(file)
checks if a file is an avi file
Definition: mythburn.py:4085
def nonfatalError(msg)
Definition: mythburn.py:336
def total_mv2_brl(files, rate)
returns total size of bitrate-limited m2v files
Definition: mythburn.py:2584
def encodeNuvToMPEG2(chanid, starttime, mediafile, destvideofile, folder, profile, usecutlist)
Re-encodes a nuv file to mpeg2 optionally removing commercials.
Definition: mythburn.py:2143
def doProcessFileProjectX(file, folder, count)
process a single file ready for burning using projectX to cut and demux
Definition: mythburn.py:4718
def runM2VRequantiser(source, destination, factor)
Run M2VRequantiser.
Definition: mythburn.py:2513
def calcSyncOffset(index)
Calculates the sync offset between the video and first audio stream.
Definition: mythburn.py:718
def doProcessFile(file, folder, count)
process a single file ready for burning using mythtranscode/mythreplex to cut and demux
Definition: mythburn.py:4506
def getLengthOfVideo(index)
Gets the duration of a video file from its stream info file.
Definition: mythburn.py:621
def getStreamInformation(filename, xmlFilename, lenMethod)
Creates a stream xml file for a video file.
Definition: mythburn.py:1720
def getFileInformation(file, folder)
Creates an info xml file from details in the job file or from the DB.
Definition: mythburn.py:1394
def WriteXMLToFile(myDOM, filename)
Write an xml file to disc.
Definition: mythburn.py:1539
def getEncodingProfilePath()
Get the directory where all encoder profile files are located.
Definition: mythburn.py:384
def ts2pts(time)
convert time stamp to pts
Definition: mythburn.py:1946
def getItemTempPath(itemnumber)
Creates a file path where the temp files for a video file can be created.
Definition: mythburn.py:442
def getVideoCodec(folder)
gets video stream codec from the stream info xml file
Definition: mythburn.py:4390
def preProcessFile(file, folder, count)
Pre-process a single video/recording file.
Definition: mythburn.py:1557
def getStreamList(folder)
get the list of required stream ids for a file
Definition: mythburn.py:4435
def getFormatedLengthOfVideo(index)
Gets the length of a video file and returns it as a string.
Definition: mythburn.py:746
def BurnDVDISO(title)
Burns the contents of a directory to create a DVD.
Definition: mythburn.py:2299
def createDVDAuthorXMLNoMenus(screensize, numberofitems)
Creates the DVDAuthor xml file used to create an Autoplay DVD.
Definition: mythburn.py:3082
def multiplexMPEGStream(video, audio1, audio2, destination, syncOffset)
Recombines a video and one or two audio streams back together adding in the NAV packets required to c...
Definition: mythburn.py:1619
def write(text, progress=True)
Definition: mythburn.py:306
def calculateFileSizes(files)
Calculates the total size of all the video, audio and menu files.
Definition: mythburn.py:2538
def runCommand(command)
Runs an external command checking to see if the user has cancelled the DVD creation process.
Definition: mythburn.py:501
def selectAspectRatio(folder)
gets the video aspect ratio from the stream info xml file
Definition: mythburn.py:4349
def getAspectRatioOfVideo(index)
Gets the aspect ratio of a video file from its stream info file.
Definition: mythburn.py:698
def selectSubtitleStream(folder)
Definition: mythburn.py:4290
def deMultiplexMPEG2File(folder, mediafile, video, audio1, audio2)
Splits a file into the separate audio and video streams using mythreplex.
Definition: mythburn.py:2458
def processJob(job)
processes one job
Definition: mythburn.py:4923
def deleteAllFilesInFolder(folder)
Removes all the files from a directory.
Definition: mythburn.py:466
def isFileOkayForDVD(file, folder)
check if file is DVD compliant
Definition: mythburn.py:4460
def checkCancelFlag()
Check to see if the user has cancelled the DVD creation process.
Definition: mythburn.py:488
def encodeMenu(background, tempvideo, music, musiclength, tempmovie, xmlfile, finaloutput, aspectratio)
Creates a short mpeg file from a jpeg image and an ac3 sound track.
Definition: mythburn.py:527
def loadFonts(themeDOM)
Load the font defintions from a DVD theme file.
Definition: mythburn.py:1348
def copyRemote(files, tmpPath)
copy files on remote filesystems to the local filesystem
Definition: mythburn.py:4885
def getVideoSize(xmlFilename)
Gets the video width and height from a file's stream xml file.
Definition: mythburn.py:1739
def findEncodingProfile(profile)
Return an xml node from a re-encoding profile xml file for a given profile name.
Definition: mythburn.py:563
def CreateDVDISO(title)
Creates an ISO image from the contents of a directory.
Definition: mythburn.py:2279
def quoteCmdArg(arg)
Escape quotes in a command line argument.
Definition: mythburn.py:398
def createEmptyPreviewFolder(videoitem)
Creates the directory to hold the preview images for an animated menu.
Definition: mythburn.py:3259
def getVideoParams(folder)
Gets the video resolution, frames per second and aspect ratio of a video file from its stream info fi...
Definition: mythburn.py:662
def getScaledAttribute(node, attribute)
Scale a theme position/size depending on the current video mode.
Definition: mythburn.py:1005
def extractVideoFrame(source, destination, seconds)
Grabs a sequence of consecutive frames from a file.
Definition: mythburn.py:2003
def getFontPathName(fontname)
Returns the path where we can find our fonts.
Definition: mythburn.py:436
def performMPEG2Shrink(files, dvdrsize)
Uses requantiser if available to shrink the video streams so they will fit on a DVD.
Definition: mythburn.py:2605
def createDVDAuthorXML(screensize, numberofitems)
Creates the DVDAuthor xml file used to create a standard DVD with menus.
Definition: mythburn.py:2689
def secondsToFrames(seconds)
Convert a time in seconds to a frame number.
Definition: mythburn.py:514
def generateProjectXCutlist(chanid, starttime, folder)
Create a projectX cut list for a recording.
Definition: mythburn.py:1804
def simple_fix_rtl(str)
Definition: mythburn.py:242
def processFile(file, folder, count)
process a single file ready for burning using either mythtranscode/mythreplex or ProjectX as the cutt...
Definition: mythburn.py:4494
def doesFileExist(file)
Returns true/false if a given file or path exists.
Definition: mythburn.py:391
def createMenu(screensize, screendpi, numberofitems)
creates the main menu for a DVD
Definition: mythburn.py:3527
def getOptions(options)
Load the options from the options node passed in the job file.
Definition: mythburn.py:946
def paintButton(draw, bgimage, bgimagemask, node, infoDOM, itemnum, page, itemsonthispage, chapternumber, chapterlist)
Paints a button onto an image.
Definition: mythburn.py:1084
def encodeAudio(format, sourcefile, destinationfile, deletesourceafterencode)
Re-encodes an audio stream to ac3.
Definition: mythburn.py:1595
def runMythtranscode(chanid, starttime, destination, usecutlist, localfile)
Run a file though the lossless encoder optionally removing commercials.
Definition: mythburn.py:1763
def processAudio(folder)
checks to see if an audio stream need to be converted to ac3
Definition: mythburn.py:4094
def paintImage(filename, maskfilename, imageDom, destimage, stretch=True)
Paint an image on the background image.
Definition: mythburn.py:1248
def intelliDraw(drawer, text, font, containerWidth)
Splits some text into lines so it will fit into a given container.
Definition: mythburn.py:1016
def paintText(draw, image, text, node, color=None, x=None, y=None, width=None, height=None)
Paint some theme text on to an image.
Definition: mythburn.py:1167
def isResolutionOkayForDVD(videoresolution)
Returns True if the given resolution is a DVD compliant one.
Definition: mythburn.py:457
def drawThemeItem(page, itemsonthispage, itemnum, menuitem, bgimage, draw, bgimagemask, drawmask, highlightcolor, spumuxdom, spunode, numberofitems, chapternumber, chapterlist)
Draws text and graphics onto a dvd menu.
Definition: mythburn.py:3312
def createDVDAuthorXMLNoMainMenu(screensize, numberofitems)
Creates the DVDAuthor xml file used to create a DVD with no main menu.
Definition: mythburn.py:3068
def saveSetting(name, data)
Save a setting to the settings table in the DB.
Definition: mythburn.py:929
def runDVDAuthor()
Runs DVDAuthor to create a DVD file structure.
Definition: mythburn.py:2268
def getText(node)
Returns the text contents from a given XML element.
Definition: mythburn.py:406
def frameToTime(frame, fps)
Convert a frame number to a time string.
Definition: mythburn.py:759
def encodeVideoToMPEG2(source, destvideofile, video, audio1, audio2, aspectratio, profile)
Re-encodes a file to mpeg2.
Definition: mythburn.py:2047
def runProjectX(chanid, starttime, folder, usecutlist, file)
Use Project-X to cut commercials and/or demux an mpeg2 file.
Definition: mythburn.py:1839
def createDetailsPage(screensize, screendpi, numberofitems)
creates the details page for a file on a DVD
Definition: mythburn.py:3951
def selectStreams(folder)
Definition: mythburn.py:4138
def extractVideoFrames(source, destination, thumbList)
Grabs a list of single frames from a file.
Definition: mythburn.py:2033
def checkBoundaryBox(boundarybox, node)
Check if boundary box need adjusting.
Definition: mythburn.py:1324
def clearArchiveItems()
Remove all archive items from the archiveitems DB table.
Definition: mythburn.py:936
def getCPUCount()
Try to work out how many cpus we have available.
Definition: mythburn.py:360
def createVideoChaptersFixedLength(itemnum, segment, lengthofvideo)
Creates some fixed length chapter marks.
Definition: mythburn.py:834
def validateTheme(theme)
Returns True if the theme.xml file can be found for the given theme.
Definition: mythburn.py:448
def deleteEverythingInFolder(folder)
Romoves all the objects from a directory.
Definition: mythburn.py:475
def paintBackground(image, node)
Paints a background rectangle onto an image.
Definition: mythburn.py:1063
def getDefaultParametersFromMythTVDB()
Reads a load of settings from DB.
Definition: mythburn.py:869
def getTempPath()
Directory where all temporary files will be created.
Definition: mythburn.py:353
def fix_rtl
Definition: mythburn.py:250
def getFileType(folder)
gets file container type from the stream info xml file
Definition: mythburn.py:4412
def expandItemText(infoDOM, text, itemnumber, pagenumber, keynumber, chapternumber, chapterlist)
Substitutes some text from a theme file with the required values.
Definition: mythburn.py:970
def main()
The main starting point for mythburn.py.
Definition: mythburn.py:5142
def getThemeFile(theme, file)
Try to find a theme file.
Definition: mythburn.py:416
def fatalError(msg)
Display an error message and exit.
Definition: mythburn.py:322
def checkSubtitles(spumuxFile)
check the given spumux.xml file for consistancy
Definition: mythburn.py:1957
def timeStringToSeconds(formatedtime)
Convert a time string of format 00:00:00 to number of seconds.
Definition: mythburn.py:772
def getThemeConfigurationXML(theme)
Load the theme.xml file for a DVD theme.
Definition: mythburn.py:608
def generateVideoPreview(videoitem, itemonthispage, menuitem, starttime, menulength, previewfolder)
Generates the thumbnail images used to create animated menus.
Definition: mythburn.py:3270
def usage()
show usage
Definition: mythburn.py:5131
def getAudioParams(folder)
Gets the audio sample rate and number of channels of a video file from its stream info file.
Definition: mythburn.py:642