From c215d84b737f603b718848d4e0590e5d2aecfd97 Mon Sep 17 00:00:00 2001 From: Arthur Date: Fri, 4 Mar 2022 16:34:00 -0500 Subject: [PATCH] updated script entry point --- fcpx_marker_tool/__main__.py | 4 + fcpx_marker_tool/fcpx_marker_tool_v1.py | 160 ++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 fcpx_marker_tool/__main__.py create mode 100644 fcpx_marker_tool/fcpx_marker_tool_v1.py diff --git a/fcpx_marker_tool/__main__.py b/fcpx_marker_tool/__main__.py new file mode 100644 index 0000000..7b56f23 --- /dev/null +++ b/fcpx_marker_tool/__main__.py @@ -0,0 +1,4 @@ +from . import fcpx_marker_tool_v1 + +if __name__ == '__main__': + fcpx_marker_tool_v1.main() \ No newline at end of file diff --git a/fcpx_marker_tool/fcpx_marker_tool_v1.py b/fcpx_marker_tool/fcpx_marker_tool_v1.py new file mode 100644 index 0000000..b408ea6 --- /dev/null +++ b/fcpx_marker_tool/fcpx_marker_tool_v1.py @@ -0,0 +1,160 @@ +""" +This script takes FCPX .xml files as an input and prints chapter marker info +""" + +import xml.etree.ElementTree as ET +from timecode import Timecode + +def parse_xml(xml_file): + tree = ET.parse(xml_file) + root = tree.getroot() + return root + +def get_timeline_info(xml_root): + + library = xml_root.find("./library") + sequence = library.find(".//sequence") + + # grab format tag that corresponds with the current sequence + format_number = sequence.get('format') + format = xml_root.find(f"./resources/format/[@id='{format_number}']") + + timeline_info = { + "start_frame": sequence.get("tcStart"), + "timecode_format": sequence.get("tcFormat"), + "frame_rate": format.get("frameDuration") + } + + return timeline_info + +def split_and_remove_s(rational_time): + rational_time = rational_time.replace("s", "") + rational_time = tuple(map(int, rational_time.split("/"))) + + return rational_time + +def frame_rate_to_tuple(frame_rate): + # Preps framerate for timecode module, ex: the string "1001/30000s" becomes a tuple (30000, 1001) + frame_rate = split_and_remove_s(frame_rate) + frame_rate = (frame_rate[1], frame_rate[0]) + + return frame_rate + +def grab_clips(xml_root): + clips_dict = {} + clips_with_markers = xml_root.findall(".//chapter-marker/..") + + for clip in clips_with_markers: + + clip_offset = clip.get("offset") + clip_start = clip.get("start") + clip_duration = clip.get("duration") + marker_list = clip.findall("chapter-marker") + + clips_dict[clip_offset, clip_start, clip_duration] = marker_list + + return clips_dict + +def get_number_of_frames(rational_time_value, frame_rate): + + if rational_time_value == "0s" or rational_time_value is None: + return 0 + + rational_time_tuple = split_and_remove_s(rational_time_value) + number_of_frames = (rational_time_tuple[0] * frame_rate[0]) / (rational_time_tuple[1] * frame_rate[1]) + + return number_of_frames + +def grab_marker_and_clip_frames(clip_offset, clip_start, clip_duration, marker_start, timeline_start, frame_rate): + + timeline_starting_frame = get_number_of_frames(timeline_start, frame_rate) + clip_offset_frame = get_number_of_frames(clip_offset, frame_rate) + clip_duration_frames = get_number_of_frames(clip_duration, frame_rate) + marker_start_frame = get_number_of_frames(marker_start, frame_rate) + clip_start_frame = get_number_of_frames(clip_start, frame_rate) + + marker_offset_frame = int((marker_start_frame - clip_start_frame) + clip_offset_frame + timeline_starting_frame) + clip_end_frame = int(clip_offset_frame + clip_duration_frames) + + return marker_offset_frame, clip_offset_frame, clip_end_frame + +def marker_timeline_check(marker_offset_frame, clip_offset_frame, clip_end_frame): + if (marker_offset_frame >= clip_offset_frame) and (marker_offset_frame <= clip_end_frame): + return True + else: + return False + +def frames_to_timecode(frame_count, frame_rate): + + #adding 1 here because timecode module needs the amount of frames here, not a 0 based frame number + frame_count += 1 + timecode_value = Timecode(frame_rate, frames=frame_count) + + return timecode_value + +def check_for_ndf(timeline_info, frame_rate_tuple): + # sometimes the framerate will be stored as something like "3003/90000s" instead of "1001/30000s" + # so this function checks to see if there is any possibility of a match for 29.97 or 59.94 no matter how the framerate is stored + + timecode_format = timeline_info["timecode_format"] + formatted_frame_rate = frame_rate_tuple + + check_5994 = (formatted_frame_rate[0] % 60000 + formatted_frame_rate[1] % 1001) + check_2997 = (formatted_frame_rate[0] % 30000 + formatted_frame_rate[1] % 1001) + + if (check_5994 == 0) and (timecode_format == "NDF"): + formatted_frame_rate = (60, 1) + elif (check_2997 == 0) and (timecode_format == "NDF"): + formatted_frame_rate = (3000, 100) + + return formatted_frame_rate + +def input_to_path(message): + input_path = input(message) + + # Handle path being input with single quotes, common in something like VSCode when dragging and dropping + if input_path.startswith("'") and input_path.endswith("'"): + input_path = input_path[1:-1] + + return input_path.strip() + + +def generate_output(clips_dict, timeline_info): + + timeline_marker_list = [] + marker_timecode = "" + timeline_start_frame = timeline_info["start_frame"] + original_frame_rate = frame_rate_to_tuple(timeline_info["frame_rate"]) + formatted_frame_rate = check_for_ndf(timeline_info, original_frame_rate) + + for clip_offset, clip_start, clip_duration in clips_dict: + clip_marker_list = clips_dict[clip_offset, clip_start, clip_duration] + + for marker in clip_marker_list: + marker_start = marker.get("start") + marker_offset_frame, clip_offset_frame, clip_end_frame = grab_marker_and_clip_frames( + clip_offset, + clip_start, + clip_duration, + marker_start, + timeline_start_frame, + original_frame_rate + ) + if marker_timeline_check(marker_offset_frame, clip_offset_frame, clip_end_frame): + marker_timecode = frames_to_timecode(marker_offset_frame, formatted_frame_rate) + timeline_marker_list.append(str(marker_timecode)) + + # sort list and only keep unique values, necessary due FCPX assigning duplicate chapter-markers to asset-clips in .fcpxml files + timeline_marker_list = sorted(set(timeline_marker_list)) + + print(*timeline_marker_list, sep='\n') + +def main(): + xml_file = input_to_path("Enter xml file path: ") + parsed_xml = parse_xml(xml_file) + timeline_info = get_timeline_info(parsed_xml) + clips_dict = grab_clips(parsed_xml) + generate_output(clips_dict, timeline_info) + +if __name__ == "__main__": + main() \ No newline at end of file