diff --git a/make_desc.py b/make_desc.py new file mode 100644 index 0000000..5cb798d --- /dev/null +++ b/make_desc.py @@ -0,0 +1,104 @@ +# Copyright (C) 2026 by WallyHackenslacker wallyhackenslacker@noreply.git.hackenslacker.space +# +# Permission to use, copy, modify, and/or distribute this software for any purpose with or without +# fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +# FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +# OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import xml.etree.ElementTree as ET +import sys +import os.path + + +def parse_fps(value): + text = str(value).strip() + if "/" in text: + numerator, denominator = text.split("/", 1) + return float(numerator) / float(denominator) + return float(text) + + +def format_timestamp(position_frames, fps): + total_seconds = int(position_frames / fps) + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + if hours > 0: + return f"{hours}:{minutes:02d}:{seconds:02d}" + + return f"{minutes}:{seconds:02d}" + + +def parse_cli_args(argv, xml_fps): + if len(argv) < 2: + raise SystemExit("Usage: make_desc.py [fps] [output.txt]") + + input_file = argv[1] + fps = parse_fps(xml_fps) + output_file = "desc.txt" + + if len(argv) >= 3: + try: + fps = parse_fps(argv[2]) + if len(argv) >= 4: + output_file = argv[3] + except ValueError: + output_file = argv[2] + + return input_file, fps, output_file + + +# Read the input file into an XML tree. +if len(sys.argv) < 2: + raise SystemExit("Usage: make_desc.py [fps] [output.txt]") + +tree = ET.parse(sys.argv[1]) +root = tree.getroot() +input_path, framerate, output_path = parse_cli_args( + sys.argv, root.attrib.get("fps", 25.0) +) + +# Get all chains in the tree. Chains are media files and metadata. +chains = {} + +for c in tree.iter("chain"): + chain = {} + chain_id = None + + for p in c: + if p.attrib["name"] == "resource" and p.text: + chain["file"] = os.path.basename(p.text) + elif p.attrib["name"] == "kdenlive:id": + if p.text: + chain_id = int(p.text) + elif p.attrib["name"] == "length" and p.text: + chain["length"] = int(p.text) + + if chain_id is not None and "file" in chain: + chains[chain_id] = chain + +# Get all clips in the tree. Clips are media files in the timeline. +clips = [] + +for c in tree.iter("clip"): + clip = { + "position": int(c.attrib["position"]), + "bin": int(c.attrib["binid"]), + } + + clips.append(clip) + +# Sort clips by time. +clips.sort(key=lambda x: x["position"]) + +# Write the description file. +with open(output_path, "w") as f: + f.write("Track list:\n") + + for c in clips: + timestamp = format_timestamp(c["position"], framerate) + f.write(f"({timestamp}) {chains[c['bin']]['file']}\n")