"""Reordenar clipes e recalcular offsets/duração. Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto. """ import xml.etree.ElementTree as ET from typing import List from ..models import ( TimeValue, ) from .helpers import SPINE_ELEMENT_TAGS class ReorderMixin: """Reordenar clipes e recalcular offsets/duração.""" # REORDER OPERATIONS # ======================================================================== def reorder_clips( self, clip_ids: List[str], target_position: str, ripple: bool = True ) -> None: """ Move clips to a new position in the timeline. Args: clip_ids: Clips to move (maintains relative order) target_position: 'start', 'end', timecode, or 'after:clip_id'/'before:clip_id' ripple: Whether to shift other clips """ spine = self._get_spine() # Collect clips to move clips_to_move = [] for clip_id in clip_ids: clip = self.clips.get(clip_id) if clip is not None and clip in list(spine): clips_to_move.append(clip) if not clips_to_move: raise ValueError(f"No clips found matching: {clip_ids}") # Calculate total duration of moving clips total_duration = TimeValue.zero() for clip in clips_to_move: dur = self._parse_time(clip.get('duration', '0s')) total_duration = total_duration + dur # Remove clips from current positions for clip in clips_to_move: spine.remove(clip) # Determine target offset and insert index spine_children = list(spine) target_offset, insert_index = self._resolve_insert_position( target_position, spine_children ) # Insert clips at new position current_offset = target_offset for clip in clips_to_move: clip.set('offset', current_offset.to_fcpxml()) spine.insert(insert_index, clip) insert_index += 1 dur = self._parse_time(clip.get('duration', '0s')) current_offset = current_offset + dur # Recalculate all offsets if ripple if ripple: self._recalculate_offsets(spine) def _recalculate_offsets(self, spine: ET.Element) -> None: """Recalculate all clip offsets sequentially.""" current_offset = TimeValue.zero() for child in spine: if child.tag in SPINE_ELEMENT_TAGS: child.set('offset', current_offset.to_fcpxml()) duration_str = child.get('duration', '0s') duration = self._parse_time(duration_str) current_offset = current_offset + duration def _timeline_duration(self) -> 'TimeValue': """Return the total timeline duration as a TimeValue. Reads from the ```` element when available, falling back to summing all spine element durations. Extracted from ``add_music_bed`` and ``batch_add_markers`` which both computed this independently. """ sequence = self.root.find('.//sequence') if sequence is not None: dur_str = sequence.get('duration') if dur_str: return self._parse_time(dur_str) spine = self._get_spine() total = TimeValue.zero() for child in spine: if child.tag in SPINE_ELEMENT_TAGS: total = total + self._parse_time(child.get('duration', '0s')) return total def _update_sequence_duration(self) -> None: """Recompute the ```` duration from the spine content. Ripple edits (``cut_clip_ranges``, ``delete_clip``, ``split_clip``) change the total timeline length without rewriting the sequence element, so an exported file kept advertising the pre-edit duration — a 326.78s sequence still claimed 326.78s after 71s of silence was removed. This helper re-syncs the attribute to the actual spine sum. """ sequence = self.root.find('.//sequence') if sequence is None: return spine = self._get_spine() total = TimeValue.zero() for child in spine: if child.tag in SPINE_ELEMENT_TAGS: total = total + self._parse_time(child.get('duration', '0s')) sequence.set('duration', total.to_fcpxml()) # ========================================================================