#!/usr/bin/env python3 """ Parse an OVEXML project file created with Olive Video Editor. Print the ordered list of all the clips used, with the name of the footage file, the start frame and the length of the clip (in frames). This is only a proof-of-concept program, tested with a simple project composed of only one video track and one audio track, clips are simply concatenated without any transitions nor gaps. """ import base64 import lxml.etree as et import os import os.path import subprocess import sys from bs4 import BeautifulSoup as bs __author__ = "Niccolo Rigacci" __copyright__ = "Copyright 2023 Niccolo Rigacci " __license__ = "GPLv3-or-later" __email__ = "niccolo@rigacci.org" __version__ = "0.0.1" def rational_to_int(rational): num, denom = rational.split('/', 1) num = int(num) denom = int(denom) frame_n = num * 30000 / denom / 1001 return frame_n ovexml_file = None if len(sys.argv) > 1: ovexml_file = sys.argv[1] if ovexml_file is None or not os.path.exists(ovexml_file): print('Usage: %s [FILE.OVEXML]' % os.path.basename(sys.argv[0])) sys.exit(1) with open(ovexml_file, 'r') as f: xml_string = f.read() soup = bs(xml_string, 'xml') # Search all the with id="org.olivevideoeditor.Olive.sequence" sequence_nodes = soup.find_all('node', {'id' : 'org.olivevideoeditor.Olive.sequence'}) print('Found %d sequence nodes' % (len(sequence_nodes),)) for node in sequence_nodes: label = node.find('label').text.strip() print(' Sequence "%s", ptr: %s' % (label, node['ptr'])) # Search the tracks following sequence with input="track_in_X". sequence_connections = [] connections = node.find('connections') for node_connections in connections.find_all('connection'): conn_input = node_connections['input'] conn_element = node_connections['element'] conn_output = node_connections.find('output').text.strip() if conn_input.startswith('track_in_'): print(' Found connection node %s element #%s ptr: %s' % (conn_input, conn_element, conn_output)) sequence_connections.append(conn_output) #print(sequence_connections) # The sequence should contains audio and video tracks. for ptr in sequence_connections: # === Search the sequence tracks === for track in soup.find_all('node', {'id' : 'org.olivevideoeditor.Olive.track', 'ptr': ptr}): print(' Found track with ptr %s' % (track['ptr'],)) # Initialize the dictionary of footage clips used. footage_clip = {} # === Search the track arraymap_in (order of clips into the timeline) === arraymap_in = [] arraymap_in_b64 = track.find('input', {'id': 'arraymap_in'}).find('track').text.strip() arraymab_in_bin = base64.b64decode(arraymap_in_b64) for i in range(0, len(arraymab_in_bin), 4): arraymap_in.append(int.from_bytes(arraymab_in_bin[i:i+4], byteorder='little')) print(' Elements in track arraymap_in (clips ordered into the timeline): %s' % (len(arraymap_in),)) # === Search the track connections (clips) === connections = track.find('connections') if not connections: print(' No connections found in this track') continue for connection in connections.find_all('connection', {'input': 'block_in'}): element = connection['element'] clip_ptr = connection.find('output').text.strip() #print(' Resolving clip for track connection element %s, ptr %s' % (element, clip_ptr,)) for clip in soup.find_all('node', {'id' : 'org.olivevideoeditor.Olive.clip', 'ptr': clip_ptr}): length_in = clip.find('input', {'id': 'length_in'}).find('track').text.strip() media_in_in = clip.find('input', {'id': 'media_in_in'}).find('track').text.strip() #print(' Resolving connections for clip ptr %16s' % (clip_ptr,)) # === Search the clip connections (transform or volume) === clip_connections = [] for clip_connection in clip.find('connections').find_all('connection', {'input': 'buffer_in'}): ptr = clip_connection.find('output').text.strip() clip_connections.append(ptr) for ptr in clip_connections: # A clip connection can be: # * org.olivevideoeditor.Olive.transform for a video track # * org.olivevideoeditor.Olive.volume for an audio track. # === Search the transform node === #print(' Resolving footage for clip connection: %s' % (ptr,)) transform = soup.find('node', {'id' : 'org.olivevideoeditor.Olive.transform', 'ptr': ptr}) if transform: footage_ptr = transform.find('connections').find('connection', {'input': 'tex_in'}).find('output').text.strip() #print(' Found footage ptr: %s' % (footage_ptr, )) volume = soup.find('node', {'id' : 'org.olivevideoeditor.Olive.volume', 'ptr': ptr}) # For volume we should search connections => footage if volume: print(' Found "volume": Audio track? Should search for volume => connections => footage') footage_ptr = None continue # === Search the footage node === footage_found = False if footage_ptr: footages = soup.find_all('node', {'id' : 'org.olivevideoeditor.Olive.footage', 'ptr': footage_ptr}) for footage in footages: footage_label = footage.find('label').text.strip() print(' Resolved track connection element #%-3s: clip ptr %16s %16s %16s (footage %s)' % (element, clip_ptr, media_in_in, length_in, footage_label)) footage_found = True footage_clip[int(element)] = { 'ptr': clip_ptr, 'media_in': rational_to_int(media_in_in), 'length_in': rational_to_int(length_in), 'footage': footage_label } if not footage_found: print(' Cannot resolve footage for connection element #%s: clip ptr %16s %16s %16s' % (element, clip_ptr, media_in_in, length_in)) # Print the timeline ordered sequence. print(' Timeline clips sequence:') for element in arraymap_in: if element in footage_clip: f = footage_clip[element] print(' Footage clip #%-3d ptr %15s: %-26s %9.2f %9.2f' % (element, f['ptr'], f['footage'], f['media_in'], f['length_in']))