#!/usr/bin/python3 from collections import defaultdict import subprocess import lxml.etree import uuid import os import sys _CACHE = {} _IDS = defaultdict(int) def get_propery(element, name): return element.xpath('property[@name="%s"]' % name)[0].text def get_melt(): cmd = ['melt'] if 'XDG_RUNTIME_DIR' not in os.environ: os.environ['XDG_RUNTIME_DIR'] = '/tmp/runtime-pandora' if 'DISPLAY' not in os.environ: cmd = ['xvfb-run', '-a'] + cmd return cmd def melt_xml(file): out = None real_path = os.path.realpath(file) if file in _CACHE and isinstance(_CACHE[file], list): ts, out = _CACHE[file] if os.stat(real_path).st_mtime != ts: out = None if not out: cmd = get_melt() + [file, '-consumer', 'xml'] out = subprocess.check_output(cmd).decode() _CACHE[file] = [os.stat(real_path).st_mtime, out] return out class KDEnliveProject: def to_xml(self): track = self._main_tractor.xpath(".//track")[0] duration = self.get_duration() values = { "in": "0", "out": str(duration - 1) } for key, value in values.items(): track.attrib[key] = value self._sequence.attrib[key] = value self._main_tractor.attrib[key] = value self._audio_tractor.attrib[key] = value self._tree.remove(self._sequence) self._tree.append(self._sequence) self._tree.remove(self._main_bin) self._tree.append(self._main_bin) self._tree.remove(self._main_tractor) self._tree.append(self._main_tractor) xml = lxml.etree.tostring(self._tree, pretty_print=True).decode() xml = xml.replace('><', '>\n<') return "\n" + xml def __init__( self, root, width="1920", height="1080", display_aspect_num="16", display_aspect_den="9", frame_rate_num="24", frame_rate_den="1" ): self._duration = defaultdict(int) self._counters = defaultdict(int) self._uuid = '{%s}' % str(uuid.uuid1()) self._width = int(width) self._height = int(height) self._fps = int(frame_rate_num) / int(frame_rate_den) self._tree = self.get_element("mlt", attrib={ "LC_NUMERIC": "C", "producer": "main_bin", "version": "7.18.0", "root": root }, children=[ self.get_element("profile", attrib={ "frame_rate_num": str(frame_rate_num), "frame_rate_den": str(frame_rate_den), "display_aspect_den": str(display_aspect_den), "display_aspect_num": str(display_aspect_num), "colorspace": "601", "progressive": "1", "description": "%sx%s %0.2ffps" % (self._width, self._height, self._fps), "width": str(width), "height": str(height), "sample_aspect_num": "1", "sample_aspect_den": "1" }), p0 := self.get_element("producer", attrib={ "in": "0", "out": "2147483647" }, children=[ ["length", "2147483647"], ["eof", "continue"], ["resource", "black"], ["aspect_ratio", "1"], ["mlt_service", "color"], ["kdenlive:playlistid", "black_track"], ["mlt_image_format", "rgba"], ["set.test_audio", "0"], ]), a4 := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), a4e := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), t_a4 := self.get_element("tractor", children=[ ["kdenlive:audio_track", "1"], ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], self.get_element("track", attrib={"hide": "video", "producer": a4.attrib["id"]}), self.get_element("track", attrib={"hide": "video", "producer": a4e.attrib["id"]}), self.get_element("filter", [ ["window", "75"], ["max_gain", "20dB"], ["mlt_service", "volume"], ["internal_added", "237"], ["disable", "1"], ]), self.get_element("filter", [ ["channel", "-1"], ["mlt_service", "panner"], ["internal_added", "237"], ["start", "0.5"], ["disable", "1"], ]), self.get_element("filter", [ ["iec_scale", "0"], ["mlt_service", "audiolevel"], ["dbpeak", "1"], ["disable", "1"], ]), ]), a3 := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), a3e := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), t_a3 := self.get_element("tractor", children=[ ["kdenlive:audio_track", "1"], ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], self.get_element("track", attrib={"hide": "video", "producer": a3.attrib["id"]}), self.get_element("track", attrib={"hide": "video", "producer": a3e.attrib["id"]}), self.get_element("filter", [ ["window", "75"], ["max_gain", "20dB"], ["mlt_service", "volume"], ["internal_added", "237"], ["disable", "1"], ]), self.get_element("filter", [ ["channel", "-1"], ["mlt_service", "panner"], ["internal_added", "237"], ["start", "0.5"], ["disable", "1"], ]), self.get_element("filter", [ ["iec_scale", "0"], ["mlt_service", "audiolevel"], ["dbpeak", "1"], ["disable", "1"], ]), ]), a2 := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), a2e := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), t_a2 := self.get_element("tractor", children=[ ["kdenlive:audio_track", "1"], ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], self.get_element("track", attrib={"hide": "video", "producer": a2.attrib["id"]}), self.get_element("track", attrib={"hide": "video", "producer": a2e.attrib["id"]}), self.get_element("filter", [ ["window", "75"], ["max_gain", "20dB"], ["mlt_service", "volume"], ["internal_added", "237"], ["disable", "1"], ]), self.get_element("filter", [ ["channel", "-1"], ["mlt_service", "panner"], ["internal_added", "237"], ["start", "0.5"], ["disable", "1"], ]), self.get_element("filter", [ ["iec_scale", "0"], ["mlt_service", "audiolevel"], ["dbpeak", "1"], ["disable", "1"], ]), ]), a1 := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), a1e := self.get_element("playlist", children=[ ["kdenlive:audio_track", "1"], ]), t_a1 := self.get_element("tractor", children=[ ["kdenlive:audio_track", "1"], ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], self.get_element("track", attrib={"hide": "video", "producer": a1.attrib["id"]}), self.get_element("track", attrib={"hide": "video", "producer": a1e.attrib["id"]}), self.get_element("filter", [ ["window", "75"], ["max_gain", "20dB"], ["mlt_service", "volume"], ["internal_added", "237"], ["disable", "1"], ]), self.get_element("filter", [ ["channel", "-1"], ["mlt_service", "panner"], ["internal_added", "237"], ["start", "0.5"], ["disable", "1"], ]), self.get_element("filter", [ ["iec_scale", "0"], ["mlt_service", "audiolevel"], ["dbpeak", "1"], ["disable", "1"], ]), ]), v1 := self.get_element("playlist", children=[ ]), v1e := self.get_element("playlist", children=[ ]), t2 := self.get_element("tractor", attrib={ "in": "00:00:00.000", "out": "00:00:25.333" }, children=[ ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], ["kdenlive:locked_track", None], self.get_element("track", attrib={"hide": "audio", "producer": v1.attrib["id"]}), self.get_element("track", attrib={"hide": "audio", "producer": v1e.attrib["id"]}), ]), v2 := self.get_element("playlist", children=[ ]), v2e := self.get_element("playlist", children=[ ]), t3 := self.get_element("tractor", attrib={ "in": "00:00:00.000" }, children=[ ["kdenlive:trackheight", "69"], ["kdenlive:timeline_active", "1"], ["kdenlive:collapsed", "0"], ["kdenlive:thumbs_format", None], ["kdenlive:audio_rec", None], ["kdenlive:locked_track", None], self.get_element("track", attrib={"hide": "audio", "producer": v2.attrib["id"]}), self.get_element("track", attrib={"hide": "audio", "producer": v2e.attrib["id"]}), ]), sequence := self.get_element("tractor", [ ["kdenlive:uuid", self._uuid], ["kdenlive:clipname", "Sequence 1"], ["kdenlive:sequenceproperties.hasAudio", "1"], ["kdenlive:sequenceproperties.hasVideo", "1"], ["kdenlive:sequenceproperties.activeTrack", "2"], ["kdenlive:sequenceproperties.tracksCount", "4"], ["kdenlive:sequenceproperties.documentuuid", self._uuid], ["kdenlive:duration", "00:00:25:09"], ["kdenlive:maxduration", "872"], ["kdenlive:producer_type", "17"], ["kdenlive:id", self.get_counter("kdenlive:id")], ["kdenlive:clip_type", "0"], ["kdenlive:folderid", "2"], ["kdenlive:sequenceproperties.audioChannels", "2"], ["kdenlive:sequenceproperties.audioTarget", "1"], ["kdenlive:sequenceproperties.tracks", "4"], ["kdenlive:sequenceproperties.verticalzoom", "1"], ["kdenlive:sequenceproperties.videoTarget", "2"], ["kdenlive:sequenceproperties.zonein", "0"], ["kdenlive:sequenceproperties.zoneout", "75"], ["kdenlive:sequenceproperties.zoom", "8"], ["kdenlive:sequenceproperties.groups", "[]"], ["kdenlive:sequenceproperties.guides", "[]"], ["kdenlive:sequenceproperties.position", "0"], ["kdenlive:sequenceproperties.scrollPos", "0"], ["kdenlive:sequenceproperties.disablepreview", "0"], self.get_element("track", attrib={"producer": p0.attrib["id"]}), self.get_element("track", attrib={"producer": t_a4.attrib["id"]}), self.get_element("track", attrib={"producer": t_a3.attrib["id"]}), self.get_element("track", attrib={"producer": t_a2.attrib["id"]}), self.get_element("track", attrib={"producer": t_a1.attrib["id"]}), self.get_element("track", attrib={"producer": t2.attrib["id"]}), self.get_element("track", attrib={"producer": t3.attrib["id"]}), self.get_element("transition", [ ["a_track", "0"], ["b_track", "1"], ["mlt_service", "mix"], ["kdenlive_id", "mix"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("transition", [ ["a_track", "0"], ["b_track", "2"], ["mlt_service", "mix"], ["kdenlive_id", "mix"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("transition", [ ["a_track", "0"], ["b_track", "3"], ["mlt_service", "mix"], ["kdenlive_id", "mix"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("transition", [ ["a_track", "0"], ["b_track", "4"], ["mlt_service", "mix"], ["kdenlive_id", "mix"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("transition", [ ["a_track", "0"], ["b_track", "5"], ["compositing", "0"], ["distort", "0"], ["rotate_center", "0"], ["mlt_service", "qtblend"], ["kdenlive_id", "qtblend"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("transition", [ ["a_track", "0"], ["b_track", "6"], ["compositing", "0"], ["distort", "0"], ["rotate_center", "0"], ["mlt_service", "qtblend"], ["kdenlive_id", "qtblend"], ["internal_added", "237"], ["always_active", "1"], ["accepts_blanks", "1"], ["sum", "1"], ]), self.get_element("filter", [ ["window", "75"], ["max_gain", "20dB"], ["mlt_service", "volume"], ["internal_added", "237"], ["disable", "1"], ]), self.get_element("filter", [ ["channel", "-1"], ["mlt_service", "panner"], ["internal_added", "237"], ["start", "0.5"], ["disable", "1"], ]), ], { "id": self._uuid }), main_bin := self.get_element("playlist", [ ["kdenlive:folder.-1.2", "Sequences"], ["kdenlive:sequenceFolder", "2"], ["kdenlive:docproperties.kdenliveversion", "23.08.0"], self.get_element("property", attrib={"name": "kdenlive:docproperties.previewextension"}), self.get_element("property", attrib={"name": "kdenlive:docproperties.previewparameters"}), ["kdenlive:docproperties.seekOffset", "30000"], ["kdenlive:docproperties.uuid", self._uuid], ["kdenlive:docproperties.version", "1.1"], ["kdenlive:expandedFolders", None], ["kdenlive:binZoom", "4"], self.get_element("property", attrib={"name": "kdenlive:documentnotes"}), ["kdenlive:docproperties.opensequences", self._uuid], ["kdenlive:docproperties.activetimeline", self._uuid], ["xml_retain", "1"], self.get_element("entry", attrib={"producer": self._uuid, "in": "0", "out": "0"}), ], { "id": "main_bin" }), t4 := self.get_element("tractor", [ ["kdenlive:projectTractor", "1"], self.get_element("track", attrib={"producer": self._uuid}), ]) ]) self._sequence = sequence self._main_bin = main_bin self._main_tractor = t4 self._audio_tractor = t_a1 self._v1 = v1 self._v2 = v2 self._a1 = a1 self._a2 = a2 self._a3 = a3 self._a4 = a4 def get_counter(self, prefix): self._counters[prefix] += 1 return str(self._counters[prefix] - 1) def get_id(self, prefix): return prefix + self.get_counter(prefix) def get_chain(self, file, kdenlive_id=None): out = melt_xml(file) chain = lxml.etree.fromstring(out).xpath('producer')[0] chain.tag = 'chain' chain.attrib['id'] = self.get_id('chain') # TBD if kdenlive_id is None: kdenlive_id = self.get_counter("kdenlive:id") for name, value in [ ("kdenlive:file_size", os.path.getsize(file)), ("kdenlive:clipname", None), ("kdenlive:clip_type", "0"), ("kdenlive:folderid", "-1"), ("kdenlive:id", kdenlive_id), ("set.test_audio", "0"), ("set.test_image", "0"), ("xml", "was here"), ]: chain.append( self.get_element( "property", attrib={"name": name}, text=str(value) if value is not None else None ) ) mlt_service = chain.xpath('property[@name="mlt_service"]')[0] mlt_service.text = "avformat-novalidate" return chain def get_duration(self): if not self._duration: return 0 return max(self._duration.values()) def get_element(self, tag, children=[], attrib={}, text=None): element = lxml.etree.Element(tag) if tag not in ( "blank", "entry", "mlt", "profile", "property", "track", ) and "id" not in attrib: element.attrib['id'] = self.get_id(tag) if attrib: for key, value in attrib.items(): element.attrib[key] = value for child in children: if isinstance(child, list) and len(child) == 2: v = child[1] if v is not None: v = str(v) child = self.get_element("property", attrib={"name": child[0]}, text=v) if isinstance(child, dict): child = self.get_element(**child) elif isinstance(child, list): child = self.get_element(*child) element.append(child) if text is not None: element.text = text return element def get_filter(self, name, value): if name == "transparency": return [self.get_element("filter", [ ["version", "0.9"], ["mlt_service", "frei0r.transparency"], ["kdenlive_id", "frei0r.transparency"], ["0", "00:00:00.000=%s" % value], ["kdenlive:collapsed", "0"], ])] elif name == "blur": return [self.get_element("filter", [ ["mlt_service", "avfilter.avgblur"], ["kdenlive_id", "avfilter.avgblur"], ["av.sizeX", value], ["av.sizeY", value], ["planes", "7"], ["kdenlive:collapsed", "0"], ])] elif name == "mask": mask = [ self.get_element("filter", [ ["mlt_service", "frei0r.saturat0r"], ["kdenlive_id", "frei0r.saturat0r"], ["Saturation", "00:00:00.000=0.001"], ]), self.get_element("filter", [ ["mlt_service", "frei0r.select0r"], ["kdenlive_id", "frei0r.select0r"], ["Color to select", "00:00:00.000=0x000000ff"], ["Invert selection", "1"], ["Selection subspace", "0"], ["Subspace shape", "0.5"], ["Edge mode", "0.9"], ["Delta R / A / Hue", "00:00:00.000=0.381"], ["Delta G / B / Chroma", "00:00:00.000=0.772"], ["Delta B / I / I", "00:00:00.000=0.522"], ["Slope", "00:00:00.000=0.515"], ["Operation", "0.5"], ]) ] return mask elif name == "volume": return [self.get_element("filter", [ ["window", "75"], ["max_gain", "20db"], ["mlt_service", "volume"], ["kdenlive_id", "volume"], ["level", "00:00:00.000=%s" % value], ["kdenlive:collapsed", "0"], ])] else: return [ self.get_element("filter", [ ["mlt_service", name], ["kdenlive_id", name], ] + value) ] def properties(self, *props): return [ self.get_element("property", attrib={"name": name}, text=str(value) if value is not None else value) for name, value in props ] def append_clip(self, track_id, clip): if track_id == "V1": track = self._v1 elif track_id == "V2": track = self._v2 elif track_id == "A1": track = self._a1 elif track_id == "A2": track = self._a2 elif track_id == "A3": track = self._a3 elif track_id == "A4": track = self._a4 else: print('!!', track_id) frames = int(self._fps * clip['duration']) self._duration[track_id] += frames if clip.get("blank"): track.append( self.get_element("blank", attrib={ "length": str(frames), }) ) return path = clip['src'] filters = clip.get("filter", {}) #print(path, filters) chain = self.get_chain(path) id = get_propery(chain, "kdenlive:id") if track_id[0] == 'A': has_audio = False for prop in chain.xpath('property'): if prop.attrib['name'].endswith('stream.type') and prop.text == "audio": has_audio = True idx = self._tree.index(track) - 1 self._tree.insert(idx, chain) filters_ = [] if track_id == 'V': filters_.append({ self.get_element("filter", [ ["mlt_service", "qtblend"], ["kdenlive_id", "qtblend"], ["rotate_center", "1"], ["rect", "00:00:00.000=0 0 %s %s 1.000000" % (self._width, self.height)], ["rotation", "00:00:00.000=0"], ["compositing", "0"], ["distort", "0"], ["kdenlive:collapsed", "0"], ["disable", "0"], ]) }) for ft in filters.items(): filters_ += self.get_filter(*ft) if track_id[0] == 'A' and not has_audio: track.append( self.get_element("blank", attrib={ "length": str(frames), }) ) else: track.append( self.get_element("entry", attrib={ "producer": chain.attrib["id"], "in": chain.attrib["in"], "out": str(frames - 1) }, children=[ ["kdenlive:id", id], ] + filters_), ) chain = self.get_chain(path, id) self._tree.append(chain) self._main_bin.append( self.get_element("entry", attrib={ "producer": chain.attrib["id"], "in": chain.attrib["in"], "out": chain.attrib["out"], }), )