Merge commit 'dm/experimental' into test branch
[vuplus_dvbapp] / lib / python / Plugins / Extensions / DVDBurn / Process.py
1 from Components.Task import Task, Job, DiskspacePrecondition, Condition, ToolExistsPrecondition
2 from Components.Harddisk import harddiskmanager
3 from Screens.MessageBox import MessageBox
4
5 class png2yuvTask(Task):
6         def __init__(self, job, inputfile, outputfile):
7                 Task.__init__(self, job, "Creating menu video")
8                 self.setTool("png2yuv")
9                 self.args += ["-n1", "-Ip", "-f25", "-j", inputfile]
10                 self.dumpFile = outputfile
11                 self.weighting = 15
12
13         def run(self, callback):
14                 Task.run(self, callback)
15                 self.container.stdoutAvail.remove(self.processStdout)
16                 self.container.dumpToFile(self.dumpFile)
17
18         def processStderr(self, data):
19                 print "[png2yuvTask]", data[:-1]
20
21 class mpeg2encTask(Task):
22         def __init__(self, job, inputfile, outputfile):
23                 Task.__init__(self, job, "Encoding menu video")
24                 self.setTool("mpeg2enc")
25                 self.args += ["-f8", "-np", "-a2", "-o", outputfile]
26                 self.inputFile = inputfile
27                 self.weighting = 25
28                 
29         def run(self, callback):
30                 Task.run(self, callback)
31                 self.container.readFromFile(self.inputFile)
32
33         def processOutputLine(self, line):
34                 print "[mpeg2encTask]", line[:-1]
35
36 class spumuxTask(Task):
37         def __init__(self, job, xmlfile, inputfile, outputfile):
38                 Task.__init__(self, job, "Muxing buttons into menu")
39                 self.setTool("spumux")
40                 self.args += [xmlfile]
41                 self.inputFile = inputfile
42                 self.dumpFile = outputfile
43                 self.weighting = 15
44
45         def run(self, callback):
46                 Task.run(self, callback)
47                 self.container.stdoutAvail.remove(self.processStdout)
48                 self.container.dumpToFile(self.dumpFile)
49                 self.container.readFromFile(self.inputFile)
50
51         def processStderr(self, data):
52                 print "[spumuxTask]", data[:-1]
53
54 class MakeFifoNode(Task):
55         def __init__(self, job, number):
56                 Task.__init__(self, job, "Make FIFO nodes")
57                 self.setTool("mknod")
58                 nodename = self.job.workspace + "/dvd_title_%d" % number + ".mpg"
59                 self.args += [nodename, "p"]
60                 self.weighting = 10
61
62 class LinkTS(Task):
63         def __init__(self, job, sourcefile, link_name):
64                 Task.__init__(self, job, "Creating symlink for source titles")
65                 self.setTool("ln")
66                 self.args += ["-s", sourcefile, link_name]
67                 self.weighting = 10
68
69 class CopyMeta(Task):
70         def __init__(self, job, sourcefile):
71                 Task.__init__(self, job, "Copy title meta files")
72                 self.setTool("cp")
73                 from os import listdir
74                 path, filename = sourcefile.rstrip("/").rsplit("/",1)
75                 tsfiles = listdir(path)
76                 for file in tsfiles:
77                         if file.startswith(filename+"."):
78                                 self.args += [path+'/'+file]
79                 self.args += [self.job.workspace]
80                 self.weighting = 15
81
82 class DemuxTask(Task):
83         def __init__(self, job, inputfile):
84                 Task.__init__(self, job, "Demux video into ES")
85                 title = job.project.titles[job.i]
86                 self.global_preconditions.append(DiskspacePrecondition(title.estimatedDiskspace))
87                 self.setTool("projectx")
88                 self.args += [inputfile, "-demux", "-set", "ExportPanel.Streamtype.Subpicture=0", "-set", "ExportPanel.Streamtype.Teletext=0", "-out", self.job.workspace ]
89                 self.end = 300
90                 self.prog_state = 0
91                 self.weighting = 1000
92                 self.cutfile = self.job.workspace + "/cut_%d.Xcl" % (job.i+1)
93                 self.cutlist = title.cutlist
94                 self.currentPID = None
95                 self.relevantAudioPIDs = [ ]
96                 self.getRelevantAudioPIDs(title)
97                 self.generated_files = [ ]
98                 self.mplex_audiofiles = { }
99                 self.mplex_videofile = ""
100                 self.mplex_streamfiles = [ ]
101                 if len(self.cutlist) > 1:
102                         self.args += [ "-cut", self.cutfile ]
103
104         def prepare(self):
105                 self.writeCutfile()
106
107         def getRelevantAudioPIDs(self, title):
108                 for audiotrack in title.properties.audiotracks:
109                         if audiotrack.active.getValue():
110                                 self.relevantAudioPIDs.append(audiotrack.pid.getValue())
111
112         def processOutputLine(self, line):
113                 line = line[:-1]
114                 #print "[DemuxTask]", line
115                 MSG_NEW_FILE = "---> new File: "
116                 MSG_PROGRESS = "[PROGRESS] "
117                 MSG_NEW_MP2 = "++> Mpg Audio: PID 0x"
118                 MSG_NEW_AC3 = "++> AC3/DTS Audio: PID 0x"
119
120                 if line.startswith(MSG_NEW_FILE):
121                         file = line[len(MSG_NEW_FILE):]
122                         if file[0] == "'":
123                                 file = file[1:-1]
124                         self.haveNewFile(file)
125                 elif line.startswith(MSG_PROGRESS):
126                         progress = line[len(MSG_PROGRESS):]
127                         self.haveProgress(progress)
128                 elif line.startswith(MSG_NEW_MP2) or line.startswith(MSG_NEW_AC3):
129                         try:
130                                 self.currentPID = str(int(line.split(': PID 0x',1)[1].split(' ',1)[0],16))
131                         except ValueError:
132                                 print "[DemuxTask] ERROR: couldn't detect Audio PID (projectx too old?)"
133
134         def haveNewFile(self, file):
135                 print "[DemuxTask] produced file:", file, self.currentPID
136                 self.generated_files.append(file)
137                 if self.currentPID in self.relevantAudioPIDs:
138                         self.mplex_audiofiles[self.currentPID] = file
139                 elif file.endswith("m2v"):
140                         self.mplex_videofile = file
141
142         def haveProgress(self, progress):
143                 #print "PROGRESS [%s]" % progress
144                 MSG_CHECK = "check & synchronize audio file"
145                 MSG_DONE = "done..."
146                 if progress == "preparing collection(s)...":
147                         self.prog_state = 0
148                 elif progress[:len(MSG_CHECK)] == MSG_CHECK:
149                         self.prog_state += 1
150                 else:
151                         try:
152                                 p = int(progress)
153                                 p = p - 1 + self.prog_state * 100
154                                 if p > self.progress:
155                                         self.progress = p
156                         except ValueError:
157                                 pass
158
159         def writeCutfile(self):
160                 f = open(self.cutfile, "w")
161                 f.write("CollectionPanel.CutMode=4\n")
162                 for p in self.cutlist:
163                         s = p / 90000
164                         m = s / 60
165                         h = m / 60
166
167                         m %= 60
168                         s %= 60
169
170                         f.write("%02d:%02d:%02d\n" % (h, m, s))
171                 f.close()
172
173         def cleanup(self, failed):
174                 print "[DemuxTask::cleanup]"
175                 self.mplex_streamfiles = [ self.mplex_videofile ]
176                 for pid in self.relevantAudioPIDs:
177                         if pid in self.mplex_audiofiles:
178                                 self.mplex_streamfiles.append(self.mplex_audiofiles[pid])
179                 print self.mplex_streamfiles
180
181                 if failed:
182                         import os
183                         for file in self.generated_files:
184                                 try:
185                                         os.remove(file)
186                                 except OSError:
187                                         pass
188
189 class MplexTaskPostcondition(Condition):
190         def check(self, task):
191                 if task.error == task.ERROR_UNDERRUN:
192                         return True
193                 return task.error is None
194
195         def getErrorMessage(self, task):
196                 return {
197                         task.ERROR_UNDERRUN: ("Can't multiplex source video!"),
198                         task.ERROR_UNKNOWN: ("An unknown error occured!")
199                 }[task.error]
200
201 class MplexTask(Task):
202         ERROR_UNDERRUN, ERROR_UNKNOWN = range(2)
203         def __init__(self, job, outputfile, inputfiles=None, demux_task=None, weighting = 500):
204                 Task.__init__(self, job, "Mux ES into PS")
205                 self.weighting = weighting
206                 self.demux_task = demux_task
207                 self.postconditions.append(MplexTaskPostcondition())
208                 self.setTool("mplex")
209                 self.args += ["-f8", "-o", outputfile, "-v1"]
210                 if inputfiles:
211                         self.args += inputfiles
212
213         def setTool(self, tool):
214                 self.cmd = tool
215                 self.args = [tool]
216                 self.global_preconditions.append(ToolExistsPrecondition())
217                 # we don't want the ReturncodePostcondition in this case because for right now we're just gonna ignore the fact that mplex fails with a buffer underrun error on some streams (this always at the very end)
218
219         def prepare(self):
220                 self.error = None
221                 if self.demux_task:
222                         self.args += self.demux_task.mplex_streamfiles
223
224         def processOutputLine(self, line):
225                 print "[MplexTask] ", line[:-1]
226                 if line.startswith("**ERROR:"):
227                         if line.find("Frame data under-runs detected") != -1:
228                                 self.error = self.ERROR_UNDERRUN
229                         else:
230                                 self.error = self.ERROR_UNKNOWN
231
232 class RemoveESFiles(Task):
233         def __init__(self, job, demux_task):
234                 Task.__init__(self, job, "Remove temp. files")
235                 self.demux_task = demux_task
236                 self.setTool("rm")
237                 self.weighting = 10
238
239         def prepare(self):
240                 self.args += ["-f"]
241                 self.args += self.demux_task.generated_files
242                 self.args += [self.demux_task.cutfile]
243
244 class DVDAuthorTask(Task):
245         def __init__(self, job):
246                 Task.__init__(self, job, "Authoring DVD")
247                 self.weighting = 20
248                 self.setTool("dvdauthor")
249                 self.CWD = self.job.workspace
250                 self.args += ["-x", self.job.workspace+"/dvdauthor.xml"]
251                 self.menupreview = job.menupreview
252
253         def processOutputLine(self, line):
254                 print "[DVDAuthorTask] ", line[:-1]
255                 if not self.menupreview and line.startswith("STAT: Processing"):
256                         self.callback(self, [], stay_resident=True)
257                 elif line.startswith("STAT: VOBU"):
258                         try:
259                                 progress = int(line.split("MB")[0].split(" ")[-1])
260                                 if progress:
261                                         self.job.mplextask.progress = progress
262                                         print "[DVDAuthorTask] update mplextask progress:", self.job.mplextask.progress, "of", self.job.mplextask.end
263                         except:
264                                 print "couldn't set mux progress"
265
266 class DVDAuthorFinalTask(Task):
267         def __init__(self, job):
268                 Task.__init__(self, job, "dvdauthor finalize")
269                 self.setTool("dvdauthor")
270                 self.args += ["-T", "-o", self.job.workspace + "/dvd"]
271
272 class WaitForResidentTasks(Task):
273         def __init__(self, job):
274                 Task.__init__(self, job, "waiting for dvdauthor to finalize")
275                 
276         def run(self, callback):
277                 print "waiting for %d resident task(s) %s to finish..." % (len(self.job.resident_tasks),str(self.job.resident_tasks))
278                 self.callback = callback
279                 if self.job.resident_tasks == 0:
280                         callback(self, [])
281
282 class BurnTaskPostcondition(Condition):
283         RECOVERABLE = True
284         def check(self, task):
285                 if task.returncode == 0:
286                         return True
287                 elif task.error is None or task.error is task.ERROR_MINUSRWBUG:
288                         return True
289                 return False
290
291         def getErrorMessage(self, task):
292                 return {
293                         task.ERROR_NOTWRITEABLE: _("Medium is not a writeable DVD!"),
294                         task.ERROR_LOAD: _("Could not load Medium! No disc inserted?"),
295                         task.ERROR_SIZE: _("Content does not fit on DVD!"),
296                         task.ERROR_WRITE_FAILED: _("Write failed!"),
297                         task.ERROR_DVDROM: _("No (supported) DVDROM found!"),
298                         task.ERROR_ISOFS: _("Medium is not empty!"),
299                         task.ERROR_FILETOOLARGE: _("TS file is too large for ISO9660 level 1!"),
300                         task.ERROR_ISOTOOLARGE: _("ISO file is too large for this filesystem!"),
301                         task.ERROR_UNKNOWN: _("An unknown error occured!")
302                 }[task.error]
303
304 class BurnTask(Task):
305         ERROR_NOTWRITEABLE, ERROR_LOAD, ERROR_SIZE, ERROR_WRITE_FAILED, ERROR_DVDROM, ERROR_ISOFS, ERROR_FILETOOLARGE, ERROR_ISOTOOLARGE, ERROR_MINUSRWBUG, ERROR_UNKNOWN = range(10)
306         def __init__(self, job, extra_args=[], tool="growisofs"):
307                 Task.__init__(self, job, job.name)
308                 self.weighting = 500
309                 self.end = 120 # 100 for writing, 10 for buffer flush, 10 for closing disc
310                 self.postconditions.append(BurnTaskPostcondition())
311                 self.setTool(tool)
312                 self.args += extra_args
313         
314         def prepare(self):
315                 self.error = None
316
317         def processOutputLine(self, line):
318                 line = line[:-1]
319                 print "[GROWISOFS] %s" % line
320                 if line[8:14] == "done, ":
321                         self.progress = float(line[:6])
322                         print "progress:", self.progress
323                 elif line.find("flushing cache") != -1:
324                         self.progress = 100
325                 elif line.find("closing disc") != -1:
326                         self.progress = 110
327                 elif line.startswith(":-["):
328                         if line.find("ASC=30h") != -1:
329                                 self.error = self.ERROR_NOTWRITEABLE
330                         elif line.find("ASC=24h") != -1:
331                                 self.error = self.ERROR_LOAD
332                         elif line.find("SK=5h/ASC=A8h/ACQ=04h") != -1:
333                                 self.error = self.ERROR_MINUSRWBUG
334                         else:
335                                 self.error = self.ERROR_UNKNOWN
336                                 print "BurnTask: unknown error %s" % line
337                 elif line.startswith(":-("):
338                         if line.find("No space left on device") != -1:
339                                 self.error = self.ERROR_SIZE
340                         elif self.error == self.ERROR_MINUSRWBUG:
341                                 print "*sigh* this is a known bug. we're simply gonna assume everything is fine."
342                                 self.postconditions = []
343                         elif line.find("write failed") != -1:
344                                 self.error = self.ERROR_WRITE_FAILED
345                         elif line.find("unable to open64(") != -1 and line.find(",O_RDONLY): No such file or directory") != -1:
346                                 self.error = self.ERROR_DVDROM
347                         elif line.find("media is not recognized as recordable DVD") != -1:
348                                 self.error = self.ERROR_NOTWRITEABLE
349                         else:
350                                 self.error = self.ERROR_UNKNOWN
351                                 print "BurnTask: unknown error %s" % line
352                 elif line.startswith("FATAL:"):
353                         if line.find("already carries isofs!"):
354                                 self.error = self.ERROR_ISOFS
355                         else:
356                                 self.error = self.ERROR_UNKNOWN
357                                 print "BurnTask: unknown error %s" % line
358                 elif line.find("-allow-limited-size was not specified. There is no way do represent this file size. Aborting.") != -1:
359                         self.error = self.ERROR_FILETOOLARGE
360                 elif line.startswith("genisoimage: File too large."):
361                         self.error = self.ERROR_ISOTOOLARGE
362         
363         def setTool(self, tool):
364                 self.cmd = tool
365                 self.args = [tool]
366                 self.global_preconditions.append(ToolExistsPrecondition())
367
368 class RemoveDVDFolder(Task):
369         def __init__(self, job):
370                 Task.__init__(self, job, "Remove temp. files")
371                 self.setTool("rm")
372                 self.args += ["-rf", self.job.workspace]
373                 self.weighting = 10
374
375 class CheckDiskspaceTask(Task):
376         def __init__(self, job):
377                 Task.__init__(self, job, "Checking free space")
378                 totalsize = 0 # require an extra safety 50 MB
379                 maxsize = 0
380                 for title in job.project.titles:
381                         titlesize = title.estimatedDiskspace
382                         if titlesize > maxsize: maxsize = titlesize
383                         totalsize += titlesize
384                 diskSpaceNeeded = totalsize + maxsize
385                 job.estimateddvdsize = totalsize / 1024 / 1024
386                 totalsize += 50*1024*1024 # require an extra safety 50 MB
387                 self.global_preconditions.append(DiskspacePrecondition(diskSpaceNeeded))
388                 self.weighting = 5
389
390         def abort(self):
391                 self.finish(aborted = True)
392
393         def run(self, callback):
394                 self.callback = callback
395                 failed_preconditions = self.checkPreconditions(True) + self.checkPreconditions(False)
396                 if len(failed_preconditions):
397                         callback(self, failed_preconditions)
398                         return
399                 Task.processFinished(self, 0)
400
401 class PreviewTask(Task):
402         def __init__(self, job, path):
403                 Task.__init__(self, job, "Preview")
404                 self.postconditions.append(PreviewTaskPostcondition())
405                 self.job = job
406                 self.path = path
407                 self.weighting = 10
408
409         def run(self, callback):
410                 self.callback = callback
411                 if self.job.menupreview:
412                         self.previewProject()
413                 else:
414                         import Screens.Standby
415                         if Screens.Standby.inStandby:
416                                 self.previewCB(False)
417                         else:
418                                 from Tools import Notifications
419                                 Notifications.AddNotificationWithCallback(self.previewCB, MessageBox, _("Do you want to preview this DVD before burning?"), timeout = 60, default = False)
420
421         def abort(self):
422                 self.finish(aborted = True)
423         
424         def previewCB(self, answer):
425                 if answer == True:
426                         self.previewProject()
427                 else:
428                         self.closedCB(True)
429
430         def playerClosed(self):
431                 if self.job.menupreview:
432                         self.closedCB(True)
433                 else:
434                         from Tools import Notifications
435                         Notifications.AddNotificationWithCallback(self.closedCB, MessageBox, _("Do you want to burn this collection to DVD medium?") )
436
437         def closedCB(self, answer):
438                 if answer == True:
439                         Task.processFinished(self, 0)
440                 else:
441                         Task.processFinished(self, 1)
442
443         def previewProject(self):
444                 from Plugins.Extensions.DVDPlayer.plugin import DVDPlayer
445                 self.job.project.session.openWithCallback(self.playerClosed, DVDPlayer, dvd_filelist= [ self.path ])
446
447 class PreviewTaskPostcondition(Condition):
448         def check(self, task):
449                 return task.returncode == 0
450
451         def getErrorMessage(self, task):
452                 return "Cancel"
453
454 class ImagingPostcondition(Condition):
455         def check(self, task):
456                 return task.returncode == 0
457
458         def getErrorMessage(self, task):
459                 return _("Failed") + ": python-imaging"
460
461 class ImagePrepareTask(Task):
462         def __init__(self, job):
463                 Task.__init__(self, job, _("please wait, loading picture..."))
464                 self.postconditions.append(ImagingPostcondition())
465                 self.weighting = 20
466                 self.job = job
467                 self.Menus = job.Menus
468                 
469         def run(self, callback):
470                 self.callback = callback
471                 # we are doing it this weird way so that the TaskView Screen actually pops up before the spinner comes
472                 from enigma import eTimer
473                 self.delayTimer = eTimer()
474                 self.delayTimer.callback.append(self.conduct)
475                 self.delayTimer.start(10,1)
476
477         def conduct(self):
478                 try:
479                         from ImageFont import truetype
480                         from Image import open as Image_open
481                         s = self.job.project.menutemplate.settings
482                         (width, height) = s.dimensions.getValue()
483                         self.Menus.im_bg_orig = Image_open(s.menubg.getValue())
484                         if self.Menus.im_bg_orig.size != (width, height):
485                                 self.Menus.im_bg_orig = self.Menus.im_bg_orig.resize((width, height))
486                         self.Menus.fontsizes = [s.fontsize_headline.getValue(), s.fontsize_title.getValue(), s.fontsize_subtitle.getValue()]
487                         self.Menus.fonts = [(truetype(s.fontface_headline.getValue(), self.Menus.fontsizes[0])), (truetype(s.fontface_title.getValue(), self.Menus.fontsizes[1])),(truetype(s.fontface_subtitle.getValue(), self.Menus.fontsizes[2]))]
488                         Task.processFinished(self, 0)
489                 except:
490                         Task.processFinished(self, 1)
491
492 class MenuImageTask(Task):
493         def __init__(self, job, menu_count, spuxmlfilename, menubgpngfilename, highlightpngfilename):
494                 Task.__init__(self, job, "Create Menu %d Image" % menu_count)
495                 self.postconditions.append(ImagingPostcondition())
496                 self.weighting = 10
497                 self.job = job
498                 self.Menus = job.Menus
499                 self.menu_count = menu_count
500                 self.spuxmlfilename = spuxmlfilename
501                 self.menubgpngfilename = menubgpngfilename
502                 self.highlightpngfilename = highlightpngfilename
503
504         def run(self, callback):
505                 self.callback = callback
506                 #try:
507                 import ImageDraw, Image, os
508                 s = self.job.project.menutemplate.settings
509                 s_top = s.margin_top.getValue()
510                 s_bottom = s.margin_bottom.getValue()
511                 s_left = s.margin_left.getValue()
512                 s_right = s.margin_right.getValue()
513                 s_rows = s.space_rows.getValue()
514                 s_cols = s.space_cols.getValue()
515                 nr_cols = s.cols.getValue()
516                 nr_rows = s.rows.getValue()
517                 thumb_size = s.thumb_size.getValue()
518                 if thumb_size[0]:
519                         from Image import open as Image_open
520                 (s_width, s_height) = s.dimensions.getValue()
521                 fonts = self.Menus.fonts
522                 im_bg = self.Menus.im_bg_orig.copy()
523                 im_high = Image.new("P", (s_width, s_height), 0)
524                 im_high.putpalette(self.Menus.spu_palette)
525                 draw_bg = ImageDraw.Draw(im_bg)
526                 draw_high = ImageDraw.Draw(im_high)
527                 if self.menu_count == 1:
528                         headlineText = self.job.project.settings.name.getValue().decode("utf-8")
529                         headlinePos = self.getPosition(s.offset_headline.getValue(), 0, 0, s_width, s_top, draw_bg.textsize(headlineText, font=fonts[0]))
530                         draw_bg.text(headlinePos, headlineText, fill=self.Menus.color_headline, font=fonts[0])
531                 spuxml = """<?xml version="1.0" encoding="utf-8"?>
532         <subpictures>
533         <stream>
534         <spu 
535         highlight="%s"
536         transparent="%02x%02x%02x"
537         start="00:00:00.00"
538         force="yes" >""" % (self.highlightpngfilename, self.Menus.spu_palette[0], self.Menus.spu_palette[1], self.Menus.spu_palette[2])
539                 #rowheight = (self.Menus.fontsizes[1]+self.Menus.fontsizes[2]+thumb_size[1]+s_rows)
540                 menu_start_title = (self.menu_count-1)*self.job.titles_per_menu + 1
541                 menu_end_title = (self.menu_count)*self.job.titles_per_menu + 1
542                 nr_titles = len(self.job.project.titles)
543                 if menu_end_title > nr_titles:
544                         menu_end_title = nr_titles+1
545                 col = 1
546                 row = 1
547                 for title_no in range( menu_start_title , menu_end_title ):
548                         title = self.job.project.titles[title_no-1]
549                         col_width  = ( s_width  - s_left - s_right  ) / nr_cols
550                         row_height = ( s_height - s_top  - s_bottom ) / nr_rows
551                         left =   s_left + ( (col-1) * col_width ) + s_cols/2
552                         right =    left + col_width - s_cols
553                         top =     s_top + ( (row-1) * row_height) + s_rows/2
554                         bottom =    top + row_height - s_rows
555                         width = right - left
556                         height = bottom - top
557
558                         if bottom > s_height:
559                                 bottom = s_height
560                         #draw_bg.rectangle((left, top, right, bottom), outline=(255,0,0))
561                         im_cell_bg = Image.new("RGBA", (width, height),(0,0,0,0))
562                         draw_cell_bg = ImageDraw.Draw(im_cell_bg)
563                         im_cell_high = Image.new("P", (width, height), 0)
564                         im_cell_high.putpalette(self.Menus.spu_palette)
565                         draw_cell_high = ImageDraw.Draw(im_cell_high)
566
567                         if thumb_size[0]:
568                                 thumbPos = self.getPosition(s.offset_thumb.getValue(), 0, 0, width, height, thumb_size)
569                                 box = (thumbPos[0], thumbPos[1], thumbPos[0]+thumb_size[0], thumbPos[1]+thumb_size[1])
570                                 try:
571                                         thumbIm = Image_open(title.inputfile.rsplit('.',1)[0] + ".png")
572                                         im_cell_bg.paste(thumbIm,thumbPos)
573                                 except:
574                                         draw_cell_bg.rectangle(box, fill=(64,127,127,127))
575                                 border = s.thumb_border.getValue()
576                                 if border:
577                                         draw_cell_high.rectangle(box, fill=1)
578                                         draw_cell_high.rectangle((box[0]+border, box[1]+border, box[2]-border, box[3]-border), fill=0)
579
580                         titleText = title.formatDVDmenuText(s.titleformat.getValue(), title_no).decode("utf-8")
581                         titlePos = self.getPosition(s.offset_title.getValue(), 0, 0, width, height, draw_bg.textsize(titleText, font=fonts[1]))
582
583                         draw_cell_bg.text(titlePos, titleText, fill=self.Menus.color_button, font=fonts[1])
584                         draw_cell_high.text(titlePos, titleText, fill=1, font=self.Menus.fonts[1])
585                         
586                         subtitleText = title.formatDVDmenuText(s.subtitleformat.getValue(), title_no).decode("utf-8")
587                         subtitlePos = self.getPosition(s.offset_subtitle.getValue(), 0, 0, width, height, draw_cell_bg.textsize(subtitleText, font=fonts[2]))
588                         draw_cell_bg.text(subtitlePos, subtitleText, fill=self.Menus.color_button, font=fonts[2])
589
590                         del draw_cell_bg
591                         del draw_cell_high
592                         im_bg.paste(im_cell_bg,(left, top, right, bottom), mask=im_cell_bg)
593                         im_high.paste(im_cell_high,(left, top, right, bottom))
594
595                         spuxml += """
596         <button name="button%s" x0="%d" x1="%d" y0="%d" y1="%d"/>""" % (str(title_no).zfill(2),left,right,top,bottom )
597                         if col < nr_cols:
598                                 col += 1
599                         else:
600                                 col = 1
601                                 row += 1
602
603                 top = s_height - s_bottom - s_rows/2
604                 if self.menu_count < self.job.nr_menus:
605                         next_page_text = s.next_page_text.getValue().decode("utf-8")
606                         textsize = draw_bg.textsize(next_page_text, font=fonts[1])
607                         pos = ( s_width-textsize[0]-s_right, top )
608                         draw_bg.text(pos, next_page_text, fill=self.Menus.color_button, font=fonts[1])
609                         draw_high.text(pos, next_page_text, fill=1, font=fonts[1])
610                         spuxml += """
611         <button name="button_next" x0="%d" x1="%d" y0="%d" y1="%d"/>""" % (pos[0],pos[0]+textsize[0],pos[1],pos[1]+textsize[1])
612                 if self.menu_count > 1:
613                         prev_page_text = s.prev_page_text.getValue().decode("utf-8")
614                         textsize = draw_bg.textsize(prev_page_text, font=fonts[1])
615                         pos = ( (s_left+s_cols/2), top )
616                         draw_bg.text(pos, prev_page_text, fill=self.Menus.color_button, font=fonts[1])
617                         draw_high.text(pos, prev_page_text, fill=1, font=fonts[1])
618                         spuxml += """
619         <button name="button_prev" x0="%d" x1="%d" y0="%d" y1="%d"/>""" % (pos[0],pos[0]+textsize[0],pos[1],pos[1]+textsize[1])
620                 del draw_bg
621                 del draw_high
622                 fd=open(self.menubgpngfilename,"w")
623                 im_bg.save(fd,"PNG")
624                 fd.close()
625                 fd=open(self.highlightpngfilename,"w")
626                 im_high.save(fd,"PNG")
627                 fd.close()
628                 spuxml += """
629         </spu>
630         </stream>
631         </subpictures>"""
632
633                 f = open(self.spuxmlfilename, "w")
634                 f.write(spuxml)
635                 f.close()
636                 Task.processFinished(self, 0)
637                 #except:
638                         #Task.processFinished(self, 1)
639                         
640         def getPosition(self, offset, left, top, right, bottom, size):
641                 pos = [left, top]
642                 if offset[0] != -1:
643                         pos[0] += offset[0]
644                 else:
645                         pos[0] += ( (right-left) - size[0] ) / 2
646                 if offset[1] != -1:
647                         pos[1] += offset[1]
648                 else:
649                         pos[1] += ( (bottom-top) - size[1] ) / 2
650                 return tuple(pos)
651
652 class Menus:
653         def __init__(self, job):
654                 self.job = job
655                 job.Menus = self
656
657                 s = self.job.project.menutemplate.settings
658
659                 self.color_headline = tuple(s.color_headline.getValue())
660                 self.color_button = tuple(s.color_button.getValue())
661                 self.color_highlight = tuple(s.color_highlight.getValue())
662                 self.spu_palette = [ 0x60, 0x60, 0x60 ] + s.color_highlight.getValue()
663
664                 ImagePrepareTask(job)
665                 nr_titles = len(job.project.titles)
666                 
667                 job.titles_per_menu = s.cols.getValue()*s.rows.getValue()
668
669                 job.nr_menus = ((nr_titles+job.titles_per_menu-1)/job.titles_per_menu)
670
671                 #a new menu_count every 4 titles (1,2,3,4->1 ; 5,6,7,8->2 etc.)
672                 for menu_count in range(1 , job.nr_menus+1):
673                         num = str(menu_count)
674                         spuxmlfilename = job.workspace+"/spumux"+num+".xml"
675                         menubgpngfilename = job.workspace+"/dvd_menubg"+num+".png"
676                         highlightpngfilename = job.workspace+"/dvd_highlight"+num+".png"
677                         MenuImageTask(job, menu_count, spuxmlfilename, menubgpngfilename, highlightpngfilename)
678                         png2yuvTask(job, menubgpngfilename, job.workspace+"/dvdmenubg"+num+".yuv")
679                         menubgm2vfilename = job.workspace+"/dvdmenubg"+num+".mv2"
680                         mpeg2encTask(job, job.workspace+"/dvdmenubg"+num+".yuv", menubgm2vfilename)
681                         menubgmpgfilename = job.workspace+"/dvdmenubg"+num+".mpg"
682                         menuaudiofilename = s.menuaudio.getValue()
683                         MplexTask(job, outputfile=menubgmpgfilename, inputfiles = [menubgm2vfilename, menuaudiofilename], weighting = 20)
684                         menuoutputfilename = job.workspace+"/dvdmenu"+num+".mpg"
685                         spumuxTask(job, spuxmlfilename, menubgmpgfilename, menuoutputfilename)
686                 
687 def CreateAuthoringXML_singleset(job):
688         nr_titles = len(job.project.titles)
689         mode = job.project.settings.authormode.getValue()
690         authorxml = []
691         authorxml.append('<?xml version="1.0" encoding="utf-8"?>\n')
692         authorxml.append(' <dvdauthor dest="' + (job.workspace+"/dvd") + '">\n')
693         authorxml.append('  <vmgm>\n')
694         authorxml.append('   <menus lang="' + job.project.menutemplate.settings.menulang.getValue() + '">\n')
695         authorxml.append('    <pgc>\n')
696         authorxml.append('     <vob file="' + job.project.settings.vmgm.getValue() + '" />\n', )
697         if mode.startswith("menu"):
698                 authorxml.append('     <post> jump titleset 1 menu; </post>\n')
699         else:
700                 authorxml.append('     <post> jump title 1; </post>\n')
701         authorxml.append('    </pgc>\n')
702         authorxml.append('   </menus>\n')
703         authorxml.append('  </vmgm>\n')
704         authorxml.append('  <titleset>\n')
705         if mode.startswith("menu"):
706                 authorxml.append('   <menus lang="' + job.project.menutemplate.settings.menulang.getValue() + '">\n')
707                 authorxml.append('    <video aspect="4:3"/>\n')
708                 for menu_count in range(1 , job.nr_menus+1):
709                         if menu_count == 1:
710                                 authorxml.append('    <pgc entry="root">\n')
711                         else:
712                                 authorxml.append('    <pgc>\n')
713                         menu_start_title = (menu_count-1)*job.titles_per_menu + 1
714                         menu_end_title = (menu_count)*job.titles_per_menu + 1
715                         if menu_end_title > nr_titles:
716                                 menu_end_title = nr_titles+1
717                         for i in range( menu_start_title , menu_end_title ):
718                                 authorxml.append('     <button name="button' + (str(i).zfill(2)) + '"> jump title ' + str(i) +'; </button>\n')
719                         if menu_count > 1:
720                                 authorxml.append('     <button name="button_prev"> jump menu ' + str(menu_count-1) + '; </button>\n')
721                         if menu_count < job.nr_menus:
722                                 authorxml.append('     <button name="button_next"> jump menu ' + str(menu_count+1) + '; </button>\n')
723                         menuoutputfilename = job.workspace+"/dvdmenu"+str(menu_count)+".mpg"
724                         authorxml.append('     <vob file="' + menuoutputfilename + '" pause="inf"/>\n')
725                         authorxml.append('    </pgc>\n')
726                 authorxml.append('   </menus>\n')
727         authorxml.append('   <titles>\n')
728         for i in range( nr_titles ):
729                 chapters = ','.join(job.project.titles[i].getChapterMarks())
730                 title_no = i+1
731                 title_filename = job.workspace + "/dvd_title_%d.mpg" % (title_no)
732                 if job.menupreview:
733                         LinkTS(job, job.project.settings.vmgm.getValue(), title_filename)
734                 else:
735                         MakeFifoNode(job, title_no)
736                 if mode.endswith("linked") and title_no < nr_titles:
737                         post_tag = "jump title %d;" % ( title_no+1 )
738                 elif mode.startswith("menu"):
739                         post_tag = "call vmgm menu 1;"
740                 else:   post_tag = ""
741
742                 authorxml.append('    <pgc>\n')
743                 authorxml.append('     <vob file="' + title_filename + '" chapters="' + chapters + '" />\n')
744                 authorxml.append('     <post> ' + post_tag + ' </post>\n')
745                 authorxml.append('    </pgc>\n')
746
747         authorxml.append('   </titles>\n')
748         authorxml.append('  </titleset>\n')
749         authorxml.append(' </dvdauthor>\n')
750         f = open(job.workspace+"/dvdauthor.xml", "w")
751         for x in authorxml:
752                 f.write(x)
753         f.close()
754
755 def CreateAuthoringXML_multiset(job):
756         nr_titles = len(job.project.titles)
757         mode = job.project.settings.authormode.getValue()
758         authorxml = []
759         authorxml.append('<?xml version="1.0" encoding="utf-8"?>\n')
760         authorxml.append(' <dvdauthor dest="' + (job.workspace+"/dvd") + '" jumppad="yes">\n')
761         authorxml.append('  <vmgm>\n')
762         authorxml.append('   <menus lang="' + job.project.menutemplate.settings.menulang.getValue() + '">\n')
763         authorxml.append('    <video aspect="4:3"/>\n')
764         if mode.startswith("menu"):
765                 for menu_count in range(1 , job.nr_menus+1):
766                         if menu_count == 1:
767                                 authorxml.append('    <pgc>\n')
768                         else:
769                                 authorxml.append('    <pgc>\n')
770                         menu_start_title = (menu_count-1)*job.titles_per_menu + 1
771                         menu_end_title = (menu_count)*job.titles_per_menu + 1
772                         if menu_end_title > nr_titles:
773                                 menu_end_title = nr_titles+1
774                         for i in range( menu_start_title , menu_end_title ):
775                                 authorxml.append('     <button name="button' + (str(i).zfill(2)) + '"> jump titleset ' + str(i) +' title 1; </button>\n')
776                         if menu_count > 1:
777                                 authorxml.append('     <button name="button_prev"> jump menu ' + str(menu_count-1) + '; </button>\n')
778                         if menu_count < job.nr_menus:
779                                 authorxml.append('     <button name="button_next"> jump menu ' + str(menu_count+1) + '; </button>\n')
780                         menuoutputfilename = job.workspace+"/dvdmenu"+str(menu_count)+".mpg"
781                         authorxml.append('     <vob file="' + menuoutputfilename + '" pause="inf"/>\n')
782                         authorxml.append('    </pgc>\n')
783         else:
784                 authorxml.append('    <pgc>\n')
785                 authorxml.append('     <vob file="' + job.project.settings.vmgm.getValue() + '" />\n' )
786                 authorxml.append('     <post> jump titleset 1 title 1; </post>\n')
787                 authorxml.append('    </pgc>\n')
788         authorxml.append('   </menus>\n')
789         authorxml.append('  </vmgm>\n')
790
791         for i in range( nr_titles ):
792                 title = job.project.titles[i]
793                 authorxml.append('  <titleset>\n')
794                 authorxml.append('   <menus lang="' + job.project.menutemplate.settings.menulang.getValue() + '">\n')
795                 authorxml.append('    <pgc entry="root">\n')
796                 authorxml.append('     <pre>\n')
797                 authorxml.append('      jump vmgm menu entry title;\n')
798                 authorxml.append('     </pre>\n')
799                 authorxml.append('    </pgc>\n')
800                 authorxml.append('   </menus>\n')
801                 authorxml.append('   <titles>\n')
802                 for audiotrack in title.properties.audiotracks:
803                         active = audiotrack.active.getValue()
804                         if active:
805                                 format = audiotrack.format.getValue()
806                                 language = audiotrack.language.getValue()
807                                 audio_tag = '    <audio format="%s"' % format
808                                 if language != "nolang":
809                                         audio_tag += ' lang="%s"' % language
810                                 audio_tag += ' />\n'
811                                 authorxml.append(audio_tag)
812                 aspect = title.properties.aspect.getValue()
813                 video_tag = '    <video aspect="'+aspect+'"'
814                 if title.properties.widescreen.getValue() == "4:3":
815                         video_tag += ' widescreen="'+title.properties.widescreen.getValue()+'"'
816                 video_tag += ' />\n'
817                 authorxml.append(video_tag)
818                 chapters = ','.join(title.getChapterMarks())
819                 title_no = i+1
820                 title_filename = job.workspace + "/dvd_title_%d.mpg" % (title_no)
821                 if job.menupreview:
822                         LinkTS(job, job.project.settings.vmgm.getValue(), title_filename)
823                 else:
824                         MakeFifoNode(job, title_no)
825                 if mode.endswith("linked") and title_no < nr_titles:
826                         post_tag = "jump titleset %d title 1;" % ( title_no+1 )
827                 elif mode.startswith("menu"):
828                         post_tag = "call vmgm menu 1;"
829                 else:   post_tag = ""
830
831                 authorxml.append('    <pgc>\n')
832                 authorxml.append('     <vob file="' + title_filename + '" chapters="' + chapters + '" />\n')
833                 authorxml.append('     <post> ' + post_tag + ' </post>\n')
834                 authorxml.append('    </pgc>\n')
835                 authorxml.append('   </titles>\n')
836                 authorxml.append('  </titleset>\n')
837         authorxml.append(' </dvdauthor>\n')
838         f = open(job.workspace+"/dvdauthor.xml", "w")
839         for x in authorxml:
840                 f.write(x)
841         f.close()
842
843 def getISOfilename(isopath, volName):
844         from Tools.Directories import fileExists
845         i = 0
846         filename = isopath+'/'+volName+".iso"
847         while fileExists(filename):
848                 i = i+1
849                 filename = isopath+'/'+volName + str(i).zfill(3) + ".iso"
850         return filename
851
852 class DVDJob(Job):
853         def __init__(self, project, menupreview=False):
854                 Job.__init__(self, "DVDBurn Job")
855                 self.project = project
856                 from time import strftime
857                 from Tools.Directories import SCOPE_HDD, resolveFilename, createDir
858                 new_workspace = resolveFilename(SCOPE_HDD) + "tmp/" + strftime("%Y%m%d%H%M%S")
859                 createDir(new_workspace, True)
860                 self.workspace = new_workspace
861                 self.project.workspace = self.workspace
862                 self.menupreview = menupreview
863                 self.conduct()
864
865         def conduct(self):
866                 CheckDiskspaceTask(self)
867                 if self.project.settings.authormode.getValue().startswith("menu") or self.menupreview:
868                         Menus(self)
869                 if self.project.settings.titlesetmode.getValue() == "multi":
870                         CreateAuthoringXML_multiset(self)
871                 else:
872                         CreateAuthoringXML_singleset(self)
873
874                 DVDAuthorTask(self)
875                 
876                 nr_titles = len(self.project.titles)
877
878                 if self.menupreview:
879                         PreviewTask(self, self.workspace + "/dvd/VIDEO_TS/")
880                 else:
881                         for self.i in range(nr_titles):
882                                 self.title = self.project.titles[self.i]
883                                 link_name =  self.workspace + "/source_title_%d.ts" % (self.i+1)
884                                 title_filename = self.workspace + "/dvd_title_%d.mpg" % (self.i+1)
885                                 LinkTS(self, self.title.inputfile, link_name)
886                                 demux = DemuxTask(self, link_name)
887                                 self.mplextask = MplexTask(self, outputfile=title_filename, demux_task=demux)
888                                 self.mplextask.end = self.estimateddvdsize
889                                 RemoveESFiles(self, demux)
890                         WaitForResidentTasks(self)
891                         PreviewTask(self, self.workspace + "/dvd/VIDEO_TS/")
892                         output = self.project.settings.output.getValue()
893                         volName = self.project.settings.name.getValue()
894                         if output == "dvd":
895                                 self.name = _("Burn DVD")
896                                 tool = "growisofs"
897                                 burnargs = [ "-Z", "/dev/" + harddiskmanager.getCD(), "-dvd-compat" ]
898                                 if self.project.size/(1024*1024) > self.project.MAX_SL:
899                                         burnargs += [ "-use-the-force-luke=4gms", "-speed=1", "-R" ]
900                         elif output == "iso":
901                                 self.name = _("Create DVD-ISO")
902                                 tool = "genisoimage"
903                                 isopathfile = getISOfilename(self.project.settings.isopath.getValue(), volName)
904                                 burnargs = [ "-o", isopathfile ]
905                         burnargs += [ "-dvd-video", "-publisher", "STB", "-V", volName, self.workspace + "/dvd" ]
906                         BurnTask(self, burnargs, tool)
907                 RemoveDVDFolder(self)
908
909 class DVDdataJob(Job):
910         def __init__(self, project):
911                 Job.__init__(self, "Data DVD Burn")
912                 self.project = project
913                 from time import strftime
914                 from Tools.Directories import SCOPE_HDD, resolveFilename, createDir
915                 new_workspace = resolveFilename(SCOPE_HDD) + "tmp/" + strftime("%Y%m%d%H%M%S") + "/dvd/"
916                 createDir(new_workspace, True)
917                 self.workspace = new_workspace
918                 self.project.workspace = self.workspace
919                 self.conduct()
920
921         def conduct(self):
922                 if self.project.settings.output.getValue() == "iso":
923                         CheckDiskspaceTask(self)
924                 nr_titles = len(self.project.titles)
925                 for self.i in range(nr_titles):
926                         title = self.project.titles[self.i]
927                         filename = title.inputfile.rstrip("/").rsplit("/",1)[1]
928                         link_name =  self.workspace + filename
929                         LinkTS(self, title.inputfile, link_name)
930                         CopyMeta(self, title.inputfile)
931
932                 output = self.project.settings.output.getValue()
933                 volName = self.project.settings.name.getValue()
934                 tool = "growisofs"
935                 if output == "dvd":
936                         self.name = _("Burn DVD")
937                         burnargs = [ "-Z", "/dev/" + harddiskmanager.getCD(), "-dvd-compat" ]
938                         if self.project.size/(1024*1024) > self.project.MAX_SL:
939                                 burnargs += [ "-use-the-force-luke=4gms", "-speed=1", "-R" ]
940                 elif output == "iso":
941                         tool = "genisoimage"
942                         self.name = _("Create DVD-ISO")
943                         isopathfile = getISOfilename(self.project.settings.isopath.getValue(), volName)
944                         burnargs = [ "-o", isopathfile ]
945                 if self.project.settings.dataformat.getValue() == "iso9660_1":
946                         burnargs += ["-iso-level", "1" ]
947                 elif self.project.settings.dataformat.getValue() == "iso9660_4":
948                         burnargs += ["-iso-level", "4", "-allow-limited-size" ]
949                 elif self.project.settings.dataformat.getValue() == "udf":
950                         burnargs += ["-udf", "-allow-limited-size" ]
951                 burnargs += [ "-publisher", "STB", "-V", volName, "-follow-links", self.workspace ]
952                 BurnTask(self, burnargs, tool)
953                 RemoveDVDFolder(self)
954
955 class DVDisoJob(Job):
956         def __init__(self, project, imagepath):
957                 Job.__init__(self, _("Burn DVD"))
958                 self.project = project
959                 self.menupreview = False
960                 from Tools.Directories import getSize
961                 if imagepath.endswith(".iso"):
962                         PreviewTask(self, imagepath)
963                         burnargs = [ "-Z", "/dev/" + harddiskmanager.getCD() + '='+imagepath, "-dvd-compat" ]
964                         if getSize(imagepath)/(1024*1024) > self.project.MAX_SL:
965                                 burnargs += [ "-use-the-force-luke=4gms", "-speed=1", "-R" ]
966                 else:
967                         PreviewTask(self, imagepath + "/VIDEO_TS/")
968                         volName = self.project.settings.name.getValue()
969                         burnargs = [ "-Z", "/dev/" + harddiskmanager.getCD(), "-dvd-compat" ]
970                         if getSize(imagepath)/(1024*1024) > self.project.MAX_SL:
971                                 burnargs += [ "-use-the-force-luke=4gms", "-speed=1", "-R" ]
972                         burnargs += [ "-dvd-video", "-publisher", "STB", "-V", volName, imagepath ]
973                 tool = "growisofs"
974                 BurnTask(self, burnargs, tool)