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