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