Add XML files for Premiere Pro sequences and a converter script
- Created `05_02_Trim_clips_in_the_Program_monitor_1.xml` and `05_03_Sync_locks_and_track_locks.xml` to define video sequences with associated clips and metadata. - Introduced `flake.nix` for managing dependencies and building the project. - Added `premiere_to_resolve.py` script to convert Premiere Pro XML exports for compatibility with DaVinci Resolve, including video path updates and MP4 to MOV conversion functionality.
This commit is contained in:
407
premiere_to_resolve.py
Normal file
407
premiere_to_resolve.py
Normal file
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Premiere to Resolve XML Converter
|
||||
|
||||
Processes Adobe Premiere Pro XML exports to make them compatible with DaVinci Resolve on Linux.
|
||||
Updates file paths and optionally converts MP4 files to MOV format.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import urllib.parse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, Set
|
||||
|
||||
|
||||
def extract_video_references(xml_file: str) -> Set[Tuple[str, str]]:
|
||||
"""
|
||||
Parse XML and extract all unique video file references.
|
||||
|
||||
Returns a set of tuples: (filename, pathurl)
|
||||
"""
|
||||
try:
|
||||
tree = ET.parse(xml_file)
|
||||
root = tree.getroot()
|
||||
except ET.ParseError as e:
|
||||
print(f"Error parsing XML file {xml_file}: {e}", file=sys.stderr)
|
||||
return set()
|
||||
|
||||
video_refs = set()
|
||||
|
||||
# Find all <file> elements that contain <pathurl> tags
|
||||
for file_elem in root.iter('file'):
|
||||
name_elem = file_elem.find('name')
|
||||
pathurl_elem = file_elem.find('pathurl')
|
||||
|
||||
if name_elem is not None and name_elem.text:
|
||||
filename = name_elem.text.strip()
|
||||
pathurl = ""
|
||||
|
||||
if pathurl_elem is not None and pathurl_elem.text:
|
||||
pathurl = pathurl_elem.text.strip()
|
||||
|
||||
# Only include video files (check extension)
|
||||
if filename.lower().endswith(('.mp4', '.mov', '.avi', '.mxf', '.mts', '.mkv')):
|
||||
video_refs.add((filename, pathurl))
|
||||
|
||||
return video_refs
|
||||
|
||||
|
||||
def decode_pathurl(pathurl: str) -> str:
|
||||
"""
|
||||
Decode a file:// URL path to a regular path and extract filename.
|
||||
|
||||
Example: file://localhost/C%3a/Users/.../file.mp4 -> file.mp4
|
||||
"""
|
||||
if not pathurl:
|
||||
return ""
|
||||
|
||||
try:
|
||||
# Remove file://localhost/ or file:/// prefix
|
||||
if pathurl.startswith('file://localhost/'):
|
||||
path_part = pathurl[17:] # Remove 'file://localhost/'
|
||||
elif pathurl.startswith('file:///'):
|
||||
path_part = pathurl[7:] # Remove 'file:///'
|
||||
elif pathurl.startswith('file://'):
|
||||
path_part = pathurl[7:] # Remove 'file://'
|
||||
else:
|
||||
path_part = pathurl
|
||||
|
||||
# URL decode
|
||||
decoded = urllib.parse.unquote(path_part)
|
||||
|
||||
# Extract filename
|
||||
filename = os.path.basename(decoded)
|
||||
return filename
|
||||
except Exception as e:
|
||||
print(f"Error decoding pathurl {pathurl}: {e}", file=sys.stderr)
|
||||
return ""
|
||||
|
||||
|
||||
def find_local_files(video_refs: Set[Tuple[str, str]], directory: str) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
Match XML references with files in directory.
|
||||
|
||||
Returns a dict mapping original filename to:
|
||||
{
|
||||
'local_path': actual path found,
|
||||
'original_pathurl': original pathurl from XML,
|
||||
'needs_conversion': True if MP4 needs conversion
|
||||
}
|
||||
"""
|
||||
directory_path = Path(directory)
|
||||
file_mapping = {}
|
||||
|
||||
# Create a case-insensitive mapping of files in directory
|
||||
local_files = {}
|
||||
for file_path in directory_path.iterdir():
|
||||
if file_path.is_file():
|
||||
local_files[file_path.name.lower()] = str(file_path.resolve())
|
||||
|
||||
for filename, pathurl in video_refs:
|
||||
# Try exact match first
|
||||
local_path = None
|
||||
if filename in local_files:
|
||||
local_path = local_files[filename]
|
||||
elif filename.lower() in local_files:
|
||||
local_path = local_files[filename.lower()]
|
||||
|
||||
if local_path:
|
||||
needs_conversion = filename.lower().endswith('.mp4')
|
||||
file_mapping[filename] = {
|
||||
'local_path': local_path,
|
||||
'original_pathurl': pathurl,
|
||||
'needs_conversion': needs_conversion
|
||||
}
|
||||
|
||||
return file_mapping
|
||||
|
||||
|
||||
def convert_mp4_to_mov(input_file: str, output_file: str) -> bool:
|
||||
"""
|
||||
Convert MP4 to MOV using ffmpeg with minimal quality loss.
|
||||
|
||||
Strategy:
|
||||
1. Try copy codecs first (lossless if compatible)
|
||||
2. Fallback to high-quality encoding
|
||||
"""
|
||||
# Check if output file already exists
|
||||
if os.path.exists(output_file):
|
||||
response = input(f"Output file {output_file} already exists. Overwrite? (y/n): ")
|
||||
if response.lower() != 'y':
|
||||
return False
|
||||
|
||||
# Try copy codecs first (lossless if compatible)
|
||||
cmd_copy = [
|
||||
'ffmpeg', '-i', input_file,
|
||||
'-c:v', 'copy',
|
||||
'-c:a', 'copy',
|
||||
'-y', # Overwrite output file
|
||||
output_file
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd_copy,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3600 # 1 hour timeout
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" Converted (copy codecs): {os.path.basename(input_file)} -> {os.path.basename(output_file)}")
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f" Timeout converting {input_file}", file=sys.stderr)
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("Error: ffmpeg not found. Please install ffmpeg.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Fallback to high-quality encoding
|
||||
print(f" Copy codecs not compatible, using high-quality encoding for {os.path.basename(input_file)}")
|
||||
cmd_encode = [
|
||||
'ffmpeg', '-i', input_file,
|
||||
'-c:v', 'libx264',
|
||||
'-crf', '18', # High quality (lower = better quality)
|
||||
'-preset', 'slow', # Better compression
|
||||
'-c:a', 'copy', # Copy audio
|
||||
'-y',
|
||||
output_file
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd_encode,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3600
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f" Converted (encoded): {os.path.basename(input_file)} -> {os.path.basename(output_file)}")
|
||||
return True
|
||||
else:
|
||||
print(f" Error converting {input_file}: {result.stderr}", file=sys.stderr)
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f" Timeout converting {input_file}", file=sys.stderr)
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f" Error converting {input_file}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def update_xml_paths(xml_file: str, file_mapping: Dict[str, Dict[str, str]]) -> bool:
|
||||
"""
|
||||
Update XML pathurl tags with new Linux file:// URLs.
|
||||
|
||||
Returns True if successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
tree = ET.parse(xml_file)
|
||||
root = tree.getroot()
|
||||
except ET.ParseError as e:
|
||||
print(f"Error parsing XML file {xml_file}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
updated_count = 0
|
||||
|
||||
# Find all <file> elements and update their pathurl and name if needed
|
||||
for file_elem in root.iter('file'):
|
||||
name_elem = file_elem.find('name')
|
||||
pathurl_elem = file_elem.find('pathurl')
|
||||
|
||||
if name_elem is not None and name_elem.text:
|
||||
filename = name_elem.text.strip()
|
||||
|
||||
if filename in file_mapping:
|
||||
mapping = file_mapping[filename]
|
||||
new_path = mapping['local_path']
|
||||
new_filename = os.path.basename(new_path)
|
||||
|
||||
# Update name tag if filename changed (e.g., MP4 -> MOV)
|
||||
if new_filename != filename:
|
||||
name_elem.text = new_filename
|
||||
|
||||
# Convert to file:// URL format
|
||||
# On Linux, file:// URLs should be file:///absolute/path (three slashes)
|
||||
# Encode the path properly, but preserve forward slashes
|
||||
# urllib.parse.quote with safe='/' will preserve slashes, so /path becomes /path
|
||||
# Then file:///path gives us the correct three-slash format
|
||||
encoded_path = urllib.parse.quote(new_path, safe='/')
|
||||
new_pathurl = f"file://{encoded_path}"
|
||||
|
||||
if pathurl_elem is not None:
|
||||
pathurl_elem.text = new_pathurl
|
||||
updated_count += 1
|
||||
else:
|
||||
# Create pathurl element if it doesn't exist
|
||||
pathurl_elem = ET.SubElement(file_elem, 'pathurl')
|
||||
pathurl_elem.text = new_pathurl
|
||||
updated_count += 1
|
||||
|
||||
if updated_count > 0:
|
||||
try:
|
||||
# Write back to file
|
||||
tree.write(xml_file, encoding='UTF-8', xml_declaration=True)
|
||||
print(f" Updated {updated_count} path references in {xml_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error writing XML file {xml_file}: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_ffmpeg() -> bool:
|
||||
"""Check if ffmpeg is available."""
|
||||
try:
|
||||
subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5)
|
||||
return True
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main workflow."""
|
||||
# Get current directory
|
||||
current_dir = os.getcwd()
|
||||
print(f"Working directory: {current_dir}\n")
|
||||
|
||||
# Find all XML files
|
||||
xml_files = [f for f in os.listdir(current_dir) if f.lower().endswith('.xml')]
|
||||
|
||||
if not xml_files:
|
||||
print("No XML files found in current directory.")
|
||||
return
|
||||
|
||||
print(f"Found {len(xml_files)} XML file(s):")
|
||||
for xml_file in xml_files:
|
||||
print(f" - {xml_file}")
|
||||
print()
|
||||
|
||||
# Extract all video references from XML files
|
||||
all_video_refs = set()
|
||||
xml_to_refs = {}
|
||||
|
||||
for xml_file in xml_files:
|
||||
refs = extract_video_references(xml_file)
|
||||
all_video_refs.update(refs)
|
||||
xml_to_refs[xml_file] = refs
|
||||
print(f"Found {len(refs)} video reference(s) in {xml_file}")
|
||||
|
||||
print(f"\nTotal unique video files referenced: {len(all_video_refs)}\n")
|
||||
|
||||
# Find local files
|
||||
file_mapping = find_local_files(all_video_refs, current_dir)
|
||||
|
||||
# Separate found and missing files
|
||||
found_files = set(file_mapping.keys())
|
||||
missing_files = {filename for filename, _ in all_video_refs if filename not in found_files}
|
||||
|
||||
# Show summary
|
||||
print("File status:")
|
||||
print(f" Found: {len(found_files)}")
|
||||
print(f" Missing: {len(missing_files)}")
|
||||
|
||||
if found_files:
|
||||
print("\nFound files:")
|
||||
for filename in sorted(found_files):
|
||||
mapping = file_mapping[filename]
|
||||
status = " (needs MP4→MOV conversion)" if mapping['needs_conversion'] else ""
|
||||
print(f" - {filename}{status}")
|
||||
|
||||
if missing_files:
|
||||
print("\nMissing files:")
|
||||
for filename in sorted(missing_files):
|
||||
print(f" - {filename}")
|
||||
|
||||
print()
|
||||
|
||||
# Check for MP4 files that need conversion
|
||||
mp4_files = {f: m for f, m in file_mapping.items() if m['needs_conversion']}
|
||||
|
||||
# Prompt for conversion
|
||||
should_convert = False
|
||||
if mp4_files:
|
||||
response = input(f"Convert {len(mp4_files)} MP4 file(s) to MOV? (y/n): ")
|
||||
should_convert = response.lower() == 'y'
|
||||
|
||||
if should_convert:
|
||||
if not check_ffmpeg():
|
||||
print("Error: ffmpeg not found. Please install ffmpeg to convert files.", file=sys.stderr)
|
||||
should_convert = False
|
||||
else:
|
||||
print("\nConverting files...")
|
||||
conversion_map = {}
|
||||
|
||||
for filename, mapping in mp4_files.items():
|
||||
input_path = mapping['local_path']
|
||||
# Create output filename with .mov extension
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
output_filename = f"{base_name}.mov"
|
||||
output_path = os.path.join(current_dir, output_filename)
|
||||
|
||||
if convert_mp4_to_mov(input_path, output_path):
|
||||
# Update mapping to point to converted file
|
||||
conversion_map[filename] = output_filename
|
||||
file_mapping[filename]['local_path'] = output_path
|
||||
file_mapping[filename]['needs_conversion'] = False
|
||||
|
||||
if conversion_map:
|
||||
print(f"\nSuccessfully converted {len(conversion_map)} file(s)")
|
||||
# Update file_mapping for converted files
|
||||
for original_filename, new_filename in conversion_map.items():
|
||||
if original_filename in file_mapping:
|
||||
new_path = os.path.join(current_dir, new_filename)
|
||||
file_mapping[original_filename]['local_path'] = new_path
|
||||
else:
|
||||
print("No MP4 files found that need conversion.")
|
||||
|
||||
print()
|
||||
|
||||
# Prompt for XML updates
|
||||
response = input("Update XML file paths? (y/n): ")
|
||||
should_update = response.lower() == 'y'
|
||||
|
||||
if should_update:
|
||||
print("\nUpdating XML files...")
|
||||
for xml_file in xml_files:
|
||||
# Only update paths for files referenced in this XML
|
||||
xml_specific_mapping = {
|
||||
filename: mapping
|
||||
for filename, mapping in file_mapping.items()
|
||||
if filename in {f for f, _ in xml_to_refs[xml_file]}
|
||||
}
|
||||
|
||||
if xml_specific_mapping:
|
||||
update_xml_paths(xml_file, xml_specific_mapping)
|
||||
else:
|
||||
print(f" No matching files found for {xml_file}")
|
||||
|
||||
# Final report
|
||||
print("\n" + "="*60)
|
||||
print("Final Report")
|
||||
print("="*60)
|
||||
|
||||
if missing_files:
|
||||
print(f"\nMissing files ({len(missing_files)}):")
|
||||
for filename in sorted(missing_files):
|
||||
print(f" - {filename}")
|
||||
else:
|
||||
print("\nAll referenced files were found!")
|
||||
|
||||
if should_convert and mp4_files:
|
||||
converted_count = sum(1 for f, m in file_mapping.items()
|
||||
if f in mp4_files and not m['needs_conversion'])
|
||||
print(f"\nConverted {converted_count} MP4 file(s) to MOV")
|
||||
|
||||
if should_update:
|
||||
print(f"\nUpdated {len(xml_files)} XML file(s)")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user