"""Marcadores: um, por timecode, e em lote. Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto. """ import xml.etree.ElementTree as ET from typing import Any, Dict, List, Optional from ..models import ( MarkerColor, MarkerType, TimeValue, ) from .helpers import build_marker_element class MarkersMixin: """Marcadores: um, por timecode, e em lote.""" # ======================================================================== # MARKER OPERATIONS # ======================================================================== def add_marker( self, clip_id: 'str | ET.Element', timecode: str, name: str, marker_type: "MarkerType | str" = MarkerType.STANDARD, color: Optional[MarkerColor] = None, note: Optional[str] = None ) -> ET.Element: """ Add a marker to a clip. Args: clip_id: Target clip identifier (name or ID) timecode: Position within clip (relative to clip start) name: Marker label marker_type: STANDARD, TODO, COMPLETED, or CHAPTER (enum or string) color: Optional marker color note: Optional marker note Returns: The created marker element """ clip = self._require_clip(clip_id) if isinstance(marker_type, str): marker_type = MarkerType.from_string(marker_type) time_value = self._parse_time(timecode) return build_marker_element( parent=clip, marker_type=marker_type, start=time_value.to_fcpxml(), duration=f"1/{int(self.fps)}s", name=name, note=note, ) def add_marker_at_timeline( self, timecode: str, name: str, marker_type: "MarkerType | str" = MarkerType.STANDARD, color: Optional[MarkerColor] = None, note: Optional[str] = None ) -> ET.Element: """Add a marker at a timeline position (finds the containing clip). Uses ``_find_spine_clip_at_seconds`` to walk the spine directly, avoiding the name-indexed ``self.clips`` dict which silently drops duplicate-named clips. """ if isinstance(marker_type, str): marker_type = MarkerType.from_string(marker_type) time_value = self._parse_time(timecode) target_seconds = time_value.to_seconds() clip, relative_seconds = self._find_spine_clip_at_seconds(target_seconds) relative_tc = TimeValue.from_seconds(relative_seconds, self.fps) return build_marker_element( parent=clip, marker_type=marker_type, start=relative_tc.to_fcpxml(), duration=f"1/{int(self.fps)}s", name=name, note=note, ) def batch_add_markers( self, markers: List[Dict[str, Any]], auto_at_cuts: bool = False, auto_at_intervals: Optional[str] = None ) -> List[ET.Element]: """ Add multiple markers at once. Args: markers: List of marker specs [{timecode, name, marker_type, color}] auto_at_cuts: Add marker at every cut point auto_at_intervals: Add markers at regular intervals (e.g., "00:00:30:00") Returns: List of created marker elements """ created = [] # Handle explicit markers for m in markers: marker = self.add_marker_at_timeline( timecode=m['timecode'], name=m['name'], marker_type=MarkerType.from_string(m.get('marker_type', 'standard')), color=MarkerColor[m['color'].upper()] if m.get('color') else None, note=m.get('note') ) created.append(marker) # Auto-detect at cuts — add a marker at the start of every spine clip. if auto_at_cuts: for i, clip in self._iter_spine_clips(): clip_start = clip.get('start', '0s') marker = build_marker_element( parent=clip, marker_type=MarkerType.STANDARD, start=clip_start, duration=f"1/{int(self.fps)}s", name=f"Cut {i+1}", ) created.append(marker) # Auto-detect at intervals — place markers at regular time steps. if auto_at_intervals: interval = self._parse_time(auto_at_intervals).to_seconds() total_duration = self._timeline_duration().to_seconds() if total_duration > 0: current = interval count = 1 while current < total_duration: try: clip, relative = self._find_spine_clip_at_seconds(current) except ValueError: current += interval count += 1 continue rel_tv = TimeValue.from_seconds(relative, self.fps) marker = build_marker_element( parent=clip, marker_type=MarkerType.STANDARD, start=rel_tv.to_fcpxml(), duration=f"1/{int(self.fps)}s", name=f"Marker {count}", ) created.append(marker) current += interval count += 1 return created