add early version of DVDBurn plugin. Basic functionality is working, but it needs...
authorFelix Domke <tmbinc@elitedvb.net>
Wed, 11 Jun 2008 00:07:28 +0000 (00:07 +0000)
committerFelix Domke <tmbinc@elitedvb.net>
Wed, 11 Jun 2008 00:07:28 +0000 (00:07 +0000)
lib/python/Plugins/Extensions/DVDBurn/DVDProject.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/DVDTitle.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/LICENSE [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/Makefile.am [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/Process.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/TitleCutter.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/TitleList.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/__init__.py [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/keymap.xml [new file with mode: 0644]
lib/python/Plugins/Extensions/DVDBurn/plugin.py [new file with mode: 0644]

diff --git a/lib/python/Plugins/Extensions/DVDBurn/DVDProject.py b/lib/python/Plugins/Extensions/DVDBurn/DVDProject.py
new file mode 100644 (file)
index 0000000..e7876e4
--- /dev/null
@@ -0,0 +1,20 @@
+class DVDProject:
+       def __init__(self):
+               self.titles = [ ]
+               self.target = None
+               self.name = _("New DVD")
+
+       def addService(self, service):
+               import DVDTitle
+               t = DVDTitle.DVDTitle()
+               t.source = service
+
+               from enigma import eServiceCenter, iServiceInformation
+               serviceHandler = eServiceCenter.getInstance()
+
+               info = serviceHandler.info(service)
+               descr = info and " " + info.getInfoString(service, iServiceInformation.sDescription) or ""
+               t.name = info and info.getName(service) or "Title" + descr
+
+               self.titles.append(t)
+               return t
diff --git a/lib/python/Plugins/Extensions/DVDBurn/DVDTitle.py b/lib/python/Plugins/Extensions/DVDBurn/DVDTitle.py
new file mode 100644 (file)
index 0000000..bc343e7
--- /dev/null
@@ -0,0 +1,9 @@
+
+class DVDTitle:
+       def __init__(self):
+               self.cutlist = [ ]
+               self.source = None
+               self.name = ""
+
+       def estimateDiskspace(self):
+               return 0
diff --git a/lib/python/Plugins/Extensions/DVDBurn/LICENSE b/lib/python/Plugins/Extensions/DVDBurn/LICENSE
new file mode 100644 (file)
index 0000000..9970059
--- /dev/null
@@ -0,0 +1,12 @@
+This plugin is licensed under the Creative Commons 
+Attribution-NonCommercial-ShareAlike 3.0 Unported 
+License. To view a copy of this license, visit
+http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to Creative
+Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.
+
+Alternatively, this plugin may be distributed and executed on hardware which
+is licensed by Dream Multimedia GmbH.
+
+This plugin is NOT free software. It is open source, you are allowed to
+modify it (if you keep the license), but it may not be commercially 
+distributed other than under the conditions noted above.
diff --git a/lib/python/Plugins/Extensions/DVDBurn/Makefile.am b/lib/python/Plugins/Extensions/DVDBurn/Makefile.am
new file mode 100644 (file)
index 0000000..0465977
--- /dev/null
@@ -0,0 +1,8 @@
+installdir = $(LIBDIR)/enigma2/python/Plugins/Extensions/DVDBurn
+
+install_PYTHON =       \
+       __init__.py \
+       plugin.py \
+       DVDProject.py DVDTitle.py TitleCutter.py TitleList.py Process.py
+
+install_DATA = keymap.xml
diff --git a/lib/python/Plugins/Extensions/DVDBurn/Process.py b/lib/python/Plugins/Extensions/DVDBurn/Process.py
new file mode 100644 (file)
index 0000000..9163b8a
--- /dev/null
@@ -0,0 +1,248 @@
+from Components.Task import Task, Job, job_manager, DiskspacePrecondition, Condition
+
+class DemuxTask(Task):
+       def __init__(self, job, inputfile, cutlist):
+               Task.__init__(self, job, "Demux video into ES")
+
+               self.global_preconditions.append(DiskspacePrecondition(4*1024*1024))
+               self.setTool("/opt/bin/projectx")
+               self.cutfile = self.job.workspace + "/cut.Xcl"
+               self.generated_files = [ ]
+               self.cutlist = cutlist
+
+               self.end = 300
+               self.prog_state = 0
+               self.weighting = 1000
+
+               self.args += [inputfile, "-demux", "-out", self.job.workspace, "-cut", self.job.workspace + "/" + self.cutfile ]
+
+       def prepare(self):
+               self.writeCutfile()
+
+       def processOutputLine(self, line):
+               line = line[:-1]
+               MSG_NEW_FILE = "---> new File: "
+               MSG_PROGRESS = "[PROGRESS] "
+
+               if line.startswith(MSG_NEW_FILE):
+                       file = line[len(MSG_NEW_FILE):]
+                       if file[0] == "'":
+                               file = file[1:-1]
+                       self.haveNewFile(file)
+               elif line.startswith(MSG_PROGRESS):
+                       progress = line[len(MSG_PROGRESS):]
+                       self.haveProgress(progress)
+
+       def haveNewFile(self, file):
+               print "PRODUCED FILE [%s]" % file
+               self.generated_files.append(file)
+
+       def haveProgress(self, progress):
+               print "PROGRESS [%s]" % progress
+               MSG_CHECK = "check & synchronize audio file"
+               MSG_DONE = "done..."
+               if progress == "preparing collection(s)...":
+                       self.prog_state = 0
+               elif progress[:len(MSG_CHECK)] == MSG_CHECK:
+                       self.prog_state += 1
+               else:
+                       try:
+                               print "have progress:", progress
+                               p = int(progress)
+                               p = p - 1 + self.prog_state * 100
+                               if p > self.progress:
+                                       self.progress = p
+                       except ValueError:
+                               print "val error"
+                               pass
+
+       def writeCutfile(self):
+               f = open(self.cutfile, "w")
+               f.write("CollectionPanel.CutMode=4\n")
+               for p in self.cutlist:
+                       s = p / 90000
+                       m = s / 60
+                       h = m / 60
+
+                       m %= 60
+                       s %= 60
+
+                       f.write("%02d:%02d:%02d\n" % (h, m, s))
+               f.close()
+
+       def cleanup(self, failed):
+               if failed:
+                       import os
+                       for f in self.generated_files:
+                               os.remove(f)
+
+class MplexTask(Task):
+       def __init__(self, job, outputfile, demux_task):
+               Task.__init__(self, job, "Mux ES into PS")
+
+               self.weighting = 500
+               self.demux_task = demux_task
+               self.setTool("/usr/bin/mplex")
+               self.args += ["-f8", "-o", self.job.workspace + "/" + outputfile, "-v1"]
+
+       def prepare(self):
+               self.args += self.demux_task.generated_files
+
+class RemoveESFiles(Task):
+       def __init__(self, job, demux_task):
+               Task.__init__(self, job, "Remove temp. files")
+               self.demux_task = demux_task
+               self.setTool("/bin/rm")
+
+       def prepare(self):
+               self.args += ["-f"]
+               self.args += self.demux_task.generated_files
+               self.args += [self.demux_task.cutfile]
+
+class DVDAuthorTask(Task):
+       def __init__(self, job, inputfiles, chapterlist):
+               Task.__init__(self, job, "dvdauthor")
+
+               self.weighting = 300
+               self.setTool("/usr/bin/dvdauthor")
+               chapterargs = "--chapters=" + ','.join(["%d:%02d:%02d.%03d" % (p / (90000 * 3600), p % (90000 * 3600) / (90000 * 60), p % (90000 * 60) / 90000, (p % 90000) / 90) for p in chapterlist])
+               self.args += ["-t", chapterargs, "-o", self.job.workspace + "/dvd", "-f"] + inputfiles
+
+class RemovePSFile(Task):
+       def __init__(self, job, psfile):
+               Task.__init__(self, job, "Remove temp. files")
+               self.setTool("/bin/rm")
+               self.args += ["-f", psfile]
+
+class DVDAuthorFinalTask(Task):
+       def __init__(self, job):
+               Task.__init__(self, job, "dvdauthor finalize")
+               self.setTool("/usr/bin/dvdauthor")
+               self.args += ["-T", "-o", self.job.workspace + "/dvd"]
+
+class BurnTaskPostcondition(Condition):
+       def check(self, task):
+               return task.error is None
+
+       def getErrorMessage(self, task):
+               return {
+                       task.ERROR_MEDIA: ("Medium is not a writeable DVD!"),
+                       task.ERROR_SIZE: ("Content does not fit on DVD!"),
+                       task.ERROR_WRITE_FAILED: ("Write failed!"),
+                       task.ERROR_DVDROM: ("No (supported) DVDROM found!"),
+                       task.ERROR_UNKNOWN: ("An unknown error occured!")
+               }[task.error]
+
+class BurnTask(Task):
+       ERROR_MEDIA, ERROR_SIZE, ERROR_WRITE_FAILED, ERROR_DVDROM, ERROR_UNKNOWN = range(5)
+       def __init__(self, job):
+               Task.__init__(self, job, "burn")
+
+               self.weighting = 500
+               self.end = 120 # 100 for writing, 10 for buffer flush, 10 for closing disc
+               self.postconditions.append(BurnTaskPostcondition())
+               self.setTool("/bin/growisofs")
+               self.args += ["-dvd-video", "-dvd-compat", "-Z", "/dev/cdroms/cdrom0", "-V", "Dreambox_DVD", "-use-the-force-luke=dummy", self.job.workspace + "/dvd"]
+
+       def prepare(self):
+               self.error = None
+
+       def processOutputLine(self, line):
+               line = line[:-1]
+               print "[GROWISOFS] %s" % line
+               if line[8:14] == "done, ":
+                       self.progress = float(line[:6])
+                       print "progress:", self.progress
+               elif line.find("flushing cache") != -1:
+                       self.progress = 100
+               elif line.find("closing disc") != -1:
+                       self.progress = 110
+               elif line.startswith(":-["):
+                       if line.find("ASC=30h") != -1:
+                               self.error = self.ERROR_MEDIA
+                       else:
+                               self.error = self.ERROR_UNKNOWN
+                               print "BurnTask: unknown error %s" % line
+               elif line.startswith(":-("):
+                       if line.find("No space left on device") != -1:
+                               self.error = self.ERROR_SIZE
+                       elif line.find("write failed") != -1:
+                               self.error = self.ERROR_WRITE_FAILED
+                       elif line.find("unable to open64(\"/dev/cdroms/cdrom0\",O_RDONLY): No such file or directory") != -1: # fixme
+                               self.error = self.ERROR_DVDROM
+                       elif line.find("media is not recognized as recordable DVD") != -1:
+                               self.error = self.ERROR_MEDIA
+                       else:
+                               self.error = self.ERROR_UNKNOWN
+                               print "BurnTask: unknown error %s" % line
+
+class RemoveDVDFolder(Task):
+       def __init__(self, job):
+               Task.__init__(self, job, "Remove temp. files")
+               self.setTool("/bin/rm")
+               self.args += ["-rf", self.job.workspace + "/dvd"]
+
+class DVDJob(Job):
+       def __init__(self, cue):
+               Job.__init__(self, "DVD Burn")
+               self.cue = cue
+               self.workspace = "/media/hdd/tmp"
+               self.fromDescription(self.createDescription())
+
+       def fromDescription(self, description):
+               nr_titles = int(description["nr_titles"])
+
+               for i in range(nr_titles):
+                       inputfile = description["inputfile%d" % i]
+                       cutlist_entries = description["cutlist%d_entries" % i]
+                       cutlist = [ ]
+                       for j in range(cutlist_entries):
+                               cutlist.append(int(description["cutlist%d_%d" % (i, j)]))
+
+                       chapterlist_entries = description["chapterlist%d_entries" % i]
+                       chapterlist = [ ]
+                       for j in range(chapterlist_entries):
+                               chapterlist.append(int(description["chapterlist%d_%d" % (i, j)]))
+
+                       demux = DemuxTask(self, inputfile = inputfile, cutlist = cutlist)
+
+                       title_filename =  self.workspace + "/dvd_title_%d.mpg" % i
+
+                       MplexTask(self, "dvd_title_%d.mpg" % i, demux)
+                       RemoveESFiles(self, demux)
+                       DVDAuthorTask(self, [title_filename], chapterlist = chapterlist)
+                       RemovePSFile(self, title_filename)
+               DVDAuthorFinalTask(self)
+               BurnTask(self)
+               RemoveDVDFolder(self)
+
+       def createDescription(self):
+               # self.cue is a list of titles, with 
+               #   each title being a tuple of 
+               #     inputfile,
+               #     a list of cutpoints (in,out)
+               #     a list of chaptermarks
+               # we turn this into a flat dict with
+               # nr_titles = the number of titles,
+               # cutlist%d_entries = the number of cutlist entries for title i,
+               # cutlist%d_%d = cutlist entry j for title i,
+               # chapterlist%d_entries = the number of chapters for title i,
+               # chapterlist%d_%d = chapter j for title i
+               res = { "nr_titles": len(self.cue) }
+               for i in range(len(self.cue)):
+                       c = self.cue[i]
+                       res["inputfile%d" % i] = c[0]
+                       res["cutlist%d_entries" % i] = len(c[1])
+                       for j in range(len(c[1])):
+                               res["cutlist%d_%d" % (i,j)] = c[1][j]
+
+                       res["chapterlist%d_entries" % i] = len(c[2])
+                       for j in range(len(c[2])):
+                               res["chapterlist%d_%d" % (i,j)] = c[2][j]
+               return res
+
+def Burn(session, cue):
+       print "burning cuesheet!"
+       j = DVDJob(cue)
+       job_manager.AddJob(j)
+       return j
diff --git a/lib/python/Plugins/Extensions/DVDBurn/TitleCutter.py b/lib/python/Plugins/Extensions/DVDBurn/TitleCutter.py
new file mode 100644 (file)
index 0000000..3cba54a
--- /dev/null
@@ -0,0 +1,10 @@
+from Plugins.Extensions.CutListEditor.plugin import CutListEditor
+
+class TitleCutter(CutListEditor):
+       def __init__(self, session, title):
+               CutListEditor.__init__(self, session, title.source)
+               #, title.cutlist)
+
+       def exit(self):
+               self.session.nav.stopService()
+               self.close(self.cut_list[:])
diff --git a/lib/python/Plugins/Extensions/DVDBurn/TitleList.py b/lib/python/Plugins/Extensions/DVDBurn/TitleList.py
new file mode 100644 (file)
index 0000000..2bc5172
--- /dev/null
@@ -0,0 +1,160 @@
+import DVDProject, DVDTitle, TitleList, TitleCutter
+
+from Screens.Screen import Screen
+from Components.ActionMap import HelpableActionMap, ActionMap
+from Components.Sources.List import List
+from enigma import eListboxPythonMultiContent, gFont, RT_HALIGN_LEFT
+
+class TitleList(Screen):
+
+       skin = """
+               <screen position="100,100" size="550,400" title="DVD Tool" >
+                       <widget source="titles" render="Listbox" scrollbarMode="showOnDemand" position="0,0" size="400,400">
+                               <convert type="StaticMultiList" />
+                       </widget>
+               </screen>"""
+
+       def __init__(self, session, project = None):
+               Screen.__init__(self, session)
+
+               if project is not None:
+                       self.project = project
+               else:
+                       self.newProject()
+
+               self["titleactions"] = HelpableActionMap(self, "DVDTitleList",
+                       {
+                               "addTitle": (self.addTitle, _("Add a new title"), _("Add title...")),
+                               "editTitle": (self.editTitle, _("Edit current title"), _("Edit title...")),
+                               "removeCurrentTitle": (self.removeCurrentTitle, _("Remove currently selected title"), _("Remove title")),
+                               "saveProject": (self.saveProject, _("Save current project to disk"), _("Save...")),
+                               "burnProject": (self.burnProject, _("Burn DVD"), _("Burn")),
+                       })
+
+               self["actions"] = ActionMap(["OkCancelActions"],
+                       {
+                               "cancel": self.leave
+                       })
+
+               #Action("addTitle", self.addTitle)
+
+               self["titles"] = List(list = [ ], enableWrapAround = True, item_height=50, fonts = [gFont("Regular", 20)])
+               self.updateTitleList()
+
+               #self["addTitle"] = ActionButton("titleactions", "addTitle")
+               #self["editTitle"] = ActionButton("titleactions", "editTitle")
+               #self["removeCurrentTitle"] = ActionButton("titleactions", "removeCurrentTitle")
+               #self["saveProject"] = ActionButton("titleactions", "saveProject")
+               #self["burnProject"] = ActionButton("titleactions", "burnProject")
+
+       def newProject(self):
+               self.project = DVDProject.DVDProject()
+               self.project.titles = [ ]
+
+       def addTitle(self):
+               from Screens.MovieSelection import MovieSelection
+               self.session.openWithCallback(self.selectedSource, MovieSelection)
+
+       def selectedSource(self, source):
+               if source is None:
+                       return None
+               t = self.project.addService(source)
+               self.updateTitleList()
+
+               self.editTitle(t)
+
+       def removeCurrentTitle(self):
+               title = self.getCurrentTitle()
+               if title is not None:
+                       self.project.titles.remove(title)
+                       self.updateTitleList()
+
+       def saveProject(self):
+               pass
+
+       def burnProject(self):
+               print "producing final cue sheet:"
+               cue = self.produceFinalCuesheet()
+               import Process
+               job = Process.Burn(self.session, cue)
+               print cue
+               from Screens.TaskView import JobView
+               self.session.open(JobView, job)
+
+       def updateTitleList(self):
+               res = [ ]
+               for title in self.project.titles:
+                       a = [ title, (eListboxPythonMultiContent.TYPE_TEXT, 0, 10, 400, 50, 0, RT_HALIGN_LEFT, title.name)  ]
+                       res.append(a)
+
+               self["titles"].list = res
+
+       def getCurrentTitle(self):
+               t = self["titles"].getCurrent()
+               return t and t[0]
+
+       def editTitle(self, title = None):
+               t = title or self.getCurrentTitle()
+               if t is not None:
+                       self.current_edit_title = t
+                       self.session.openWithCallback(self.titleEditDone, TitleCutter.TitleCutter, t)
+
+       def titleEditDone(self, cutlist):
+               t = self.current_edit_title
+               t.cutlist = cutlist
+               print "title edit of %s done, resulting cutlist:" % (t.source.toString()), t.cutlist
+
+       def leave(self):
+               self.close()
+
+       def produceFinalCuesheet(self):
+               res = [ ]
+               for title in self.project.titles:
+                       path = title.source.getPath()
+                       print ">>> path:", path
+                       cutlist = title.cutlist
+
+                       # our demuxer expects *stricly* IN,OUT lists.
+                       first = True
+                       currently_in = False
+                       CUT_TYPE_IN = 0
+                       CUT_TYPE_OUT = 1
+                       CUT_TYPE_MARK = 2
+                       CUT_TYPE_LAST = 3
+
+                       accumulated_in = 0
+                       accumulated_at = 0
+                       last_in = 0
+
+                       res_cutlist = [ ]
+
+                       res_chaptermarks = [0]
+
+                       for (pts, type) in cutlist:
+                               if first and type == CUT_TYPE_OUT: # first mark is "out"
+                                       res_cutlist.append(0) # emulate "in" at first
+                                       currently_in = True
+
+                               first = False
+
+                               if type == CUT_TYPE_IN and not currently_in:
+                                       res_cutlist.append(pts)
+                                       last_in = pts
+                                       currently_in = True
+
+                               if type == CUT_TYPE_OUT and currently_in:
+                                       res_cutlist.append(pts)
+
+                                       # accumulate the segment
+                                       accumulated_in += pts - last_in 
+                                       accumulated_at = pts
+                                       currently_in = False
+
+                               if type == CUT_TYPE_MARK and currently_in:
+                                       # relocate chaptermark against "in" time. This is not 100% accurate,
+                                       # as the in/out points are not.
+                                       res_chaptermarks.append(pts - accumulated_at + accumulated_in)
+
+                       res.append( (path, res_cutlist, res_chaptermarks) )
+
+               return res
diff --git a/lib/python/Plugins/Extensions/DVDBurn/__init__.py b/lib/python/Plugins/Extensions/DVDBurn/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/python/Plugins/Extensions/DVDBurn/keymap.xml b/lib/python/Plugins/Extensions/DVDBurn/keymap.xml
new file mode 100644 (file)
index 0000000..6c57ba4
--- /dev/null
@@ -0,0 +1,10 @@
+<keymap>
+       <map context="DVDTitleList">
+               <key id="KEY_RED" mapto="addTitle" flags="m" />
+               <key id="KEY_GREEN" mapto="editTitle" flags="m" />
+               <key id="KEY_BLUE" mapto="removeCurrentTitle" flags="m" />
+               <key id="KEY_YELLOW" mapto="saveProject" flags="m" />
+               <key id="KEY_RECORD" mapto="burnProject" flags="m" />
+               <key id="KEY_0" mapto="burnProject" flags="m" />
+       </map>
+</keymap>
diff --git a/lib/python/Plugins/Extensions/DVDBurn/plugin.py b/lib/python/Plugins/Extensions/DVDBurn/plugin.py
new file mode 100644 (file)
index 0000000..66fe96d
--- /dev/null
@@ -0,0 +1,11 @@
+from Plugins.Plugin import PluginDescriptor
+
+def main(session, service, **kwargs):
+       import TitleList
+       import DVDProject
+       project = DVDProject.DVDProject()
+       project.addService(service)
+       session.open(TitleList.TitleList, project)
+
+def Plugins(**kwargs):
+       return PluginDescriptor(name="DVD Tool", description=_("Burn To DVD..."), where = PluginDescriptor.WHERE_MOVIELIST, fnc=main)