commit 5f146175304d6a529ca69da13050abec8236a89d Author: Danilo Reyes Date: Mon Jan 26 16:27:09 2026 -0600 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. diff --git a/05_02_Trim_clips_in_the_Program_monitor_1.xml b/05_02_Trim_clips_in_the_Program_monitor_1.xml new file mode 100644 index 0000000..29facca --- /dev/null +++ b/05_02_Trim_clips_in_the_Program_monitor_1.xml @@ -0,0 +1,2782 @@ + + + + + f96ea336-5142-46e3-a65e-92b2425909aa + 850 + + 24 + TRUE + + Going Home + + + + + + + 24 + TRUE + + 00:00:00:00 + 0 + NDF + + + Forest + + + + + + + + + + + + + Audio Levels + audiolevels + audiolevels + audiolevels + audio + + level + Level + 0 + 3.98109 + 1.63722 + + + + + diff --git a/05_03_Sync_locks_and_track_locks.xml b/05_03_Sync_locks_and_track_locks.xml new file mode 100644 index 0000000..af8b3dd --- /dev/null +++ b/05_03_Sync_locks_and_track_locks.xml @@ -0,0 +1,6307 @@ + + + + + f96ea336-5142-46e3-a65e-92b2425909aa + 1445 + + 24 + TRUE + + Going Home + + + + + + + 24 + TRUE + + 00:00:00:00 + 0 + NDF + + + Forest + + + + + + + + + + + + + Audio Levels + audiolevels + audiolevels + audiolevels + audio + + level + Level + 0 + 3.98109 + 1.63722 + + + + + diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..481168b --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + description = "Premiere to Resolve XML Converter - Convert Adobe Premiere Pro XML exports for DaVinci Resolve on Linux"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + prem2resolve = pkgs.writeScriptBin "prem2resolve" '' + #!${pkgs.python3}/bin/python3 + ${builtins.readFile ./premiere_to_resolve.py} + ''; + + prem2resolve-wrapped = pkgs.symlinkJoin { + name = "prem2resolve"; + paths = [ prem2resolve ]; + buildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/prem2resolve \ + --prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.ffmpeg ]} + ''; + }; + in + { + packages = { + default = prem2resolve-wrapped; + prem2resolve = prem2resolve-wrapped; + }; + + apps = { + default = { + type = "app"; + program = "${prem2resolve-wrapped}/bin/prem2resolve"; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = [ + pkgs.python3 + pkgs.ffmpeg + ]; + }; + } + ); +} diff --git a/premiere_to_resolve.py b/premiere_to_resolve.py new file mode 100644 index 0000000..76605d8 --- /dev/null +++ b/premiere_to_resolve.py @@ -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 elements that contain 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 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()