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