Skip to main content

tellers_timeline_core/
metadata.rs

1use crate::{Clip, Gap, Item, MediaReference, Stack, Timeline, Track};
2
3pub trait IdMetadataExt {
4    fn get_id(&self) -> Option<String>;
5    fn set_id(&mut self, id: Option<String>);
6}
7
8fn read_id_from_metadata(meta: &serde_json::Value) -> Option<String> {
9    meta.get("tellers.ai")
10        .and_then(|v| v.get("timeline_id"))
11        .and_then(|v| v.as_str())
12        .map(|s| s.to_string())
13}
14
15pub(crate) fn write_id_to_metadata(meta: &mut serde_json::Value, id: Option<String>) {
16    // Ensure we have an object at the root
17    if meta.as_object().is_none() {
18        *meta = serde_json::Value::Object(serde_json::Map::new());
19    }
20
21    let map = meta.as_object_mut().unwrap();
22    match id {
23        Some(uid) => {
24            // Ensure we have an object at metadata["tellers.ai"]
25            let ai_entry = map
26                .entry("tellers.ai".to_string())
27                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
28            if ai_entry.as_object().is_none() {
29                *ai_entry = serde_json::Value::Object(serde_json::Map::new());
30            }
31            let ai_map = ai_entry.as_object_mut().unwrap();
32            ai_map.insert("timeline_id".to_string(), serde_json::Value::String(uid));
33        }
34        None => {
35            if let Some(ai_entry) = map.get_mut("tellers.ai") {
36                if let Some(ai_map) = ai_entry.as_object_mut() {
37                    ai_map.remove("timeline_id");
38                    // If the tellers.ai object is now empty, leave it in place to preserve shape
39                }
40            }
41        }
42    }
43}
44
45impl IdMetadataExt for Clip {
46    fn get_id(&self) -> Option<String> {
47        read_id_from_metadata(&self.metadata)
48    }
49    fn set_id(&mut self, id: Option<String>) {
50        write_id_to_metadata(&mut self.metadata, id)
51    }
52}
53
54impl IdMetadataExt for Track {
55    fn get_id(&self) -> Option<String> {
56        read_id_from_metadata(&self.metadata)
57    }
58    fn set_id(&mut self, id: Option<String>) {
59        write_id_to_metadata(&mut self.metadata, id)
60    }
61}
62
63impl IdMetadataExt for Gap {
64    fn get_id(&self) -> Option<String> {
65        read_id_from_metadata(&self.metadata)
66    }
67    fn set_id(&mut self, id: Option<String>) {
68        write_id_to_metadata(&mut self.metadata, id)
69    }
70}
71
72impl IdMetadataExt for Item {
73    fn get_id(&self) -> Option<String> {
74        match self {
75            Item::Clip(c) => c.get_id(),
76            Item::Gap(g) => g.get_id(),
77        }
78    }
79    fn set_id(&mut self, id: Option<String>) {
80        match self {
81            Item::Clip(c) => c.set_id(id),
82            Item::Gap(g) => g.set_id(id),
83        }
84    }
85}
86
87pub trait MetadataExt {
88    fn get_metadata(&self) -> &serde_json::Value;
89    fn get_metadata_mut(&mut self) -> &mut serde_json::Value;
90    fn set_metadata(&mut self, metadata: serde_json::Value);
91}
92
93impl MetadataExt for Timeline {
94    fn get_metadata(&self) -> &serde_json::Value {
95        &self.metadata
96    }
97    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
98        &mut self.metadata
99    }
100    fn set_metadata(&mut self, metadata: serde_json::Value) {
101        let mut v = metadata;
102        if v.as_object().is_none() {
103            v = serde_json::Value::Object(serde_json::Map::new());
104        }
105        self.metadata = v;
106    }
107}
108
109impl MetadataExt for Stack {
110    fn get_metadata(&self) -> &serde_json::Value {
111        &self.metadata
112    }
113    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
114        &mut self.metadata
115    }
116    fn set_metadata(&mut self, metadata: serde_json::Value) {
117        let mut v = metadata;
118        if v.as_object().is_none() {
119            v = serde_json::Value::Object(serde_json::Map::new());
120        }
121        self.metadata = v;
122    }
123}
124
125impl MetadataExt for Track {
126    fn get_metadata(&self) -> &serde_json::Value {
127        &self.metadata
128    }
129    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
130        &mut self.metadata
131    }
132    fn set_metadata(&mut self, metadata: serde_json::Value) {
133        let mut v = metadata;
134        if v.as_object().is_none() {
135            v = serde_json::Value::Object(serde_json::Map::new());
136        }
137        self.metadata = v;
138    }
139}
140
141impl MetadataExt for Clip {
142    fn get_metadata(&self) -> &serde_json::Value {
143        &self.metadata
144    }
145    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
146        &mut self.metadata
147    }
148    fn set_metadata(&mut self, metadata: serde_json::Value) {
149        let mut v = metadata;
150        if v.as_object().is_none() {
151            v = serde_json::Value::Object(serde_json::Map::new());
152        }
153        self.metadata = v;
154    }
155}
156
157impl MetadataExt for Gap {
158    fn get_metadata(&self) -> &serde_json::Value {
159        &self.metadata
160    }
161    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
162        &mut self.metadata
163    }
164    fn set_metadata(&mut self, metadata: serde_json::Value) {
165        let mut v = metadata;
166        if v.as_object().is_none() {
167            v = serde_json::Value::Object(serde_json::Map::new());
168        }
169        self.metadata = v;
170    }
171}
172
173impl MetadataExt for MediaReference {
174    fn get_metadata(&self) -> &serde_json::Value {
175        self.metadata()
176    }
177    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
178        self.metadata_mut()
179    }
180    fn set_metadata(&mut self, metadata: serde_json::Value) {
181        let mut v = metadata;
182        if v.as_object().is_none() {
183            v = serde_json::Value::Object(serde_json::Map::new());
184        }
185        *self.metadata_mut() = v;
186    }
187}
188
189impl MetadataExt for Item {
190    fn get_metadata(&self) -> &serde_json::Value {
191        match self {
192            Item::Clip(c) => &c.metadata,
193            Item::Gap(g) => &g.metadata,
194        }
195    }
196    fn get_metadata_mut(&mut self) -> &mut serde_json::Value {
197        match self {
198            Item::Clip(c) => &mut c.metadata,
199            Item::Gap(g) => &mut g.metadata,
200        }
201    }
202    fn set_metadata(&mut self, metadata: serde_json::Value) {
203        let mut v = metadata;
204        if v.as_object().is_none() {
205            v = serde_json::Value::Object(serde_json::Map::new());
206        }
207        match self {
208            Item::Clip(c) => c.metadata = v,
209            Item::Gap(g) => g.metadata = v,
210        }
211    }
212}
213
214// ---- Group-id metadata accessors -------------------------------------------
215//
216// Timeline items carry two distinct grouping ids in their metadata:
217//
218// * the Resolve "Link Group ID" (`metadata["Resolve_OTIO"]["Link Group ID"]`),
219//   which ties synchronised clips together, and
220// * the Tellers Group ID (`metadata["tellers.ai"]["Tellers Group ID"]`), the
221//   Tellers-native "move together" grouping.
222//
223// These accessors expose the read/write conventions for both so callers (the
224// editor bridge, the stack methods) don't re-implement the metadata layout.
225
226/// The Resolve "Link Group ID" of an item (the sync-clip group), or `None` for
227/// an ungrouped clip or a gap.
228pub fn item_link_group_id(item: &Item) -> Option<i64> {
229    match item {
230        Item::Clip(clip) => clip.sync_clips_id(),
231        Item::Gap(_) => None,
232    }
233}
234
235/// The Tellers Group ID of an item, or `None` for an ungrouped clip or a gap.
236pub fn item_tellers_group_id(item: &Item) -> Option<i64> {
237    match item {
238        Item::Clip(clip) => resolve_tellers_group_id(&clip.metadata),
239        Item::Gap(_) => None,
240    }
241}
242
243/// Set (or clear, when `group_id` is `None`) the Tellers Group ID on an item.
244pub fn set_item_tellers_group_id(item: &mut Item, group_id: Option<i64>) {
245    let metadata = item.get_metadata_mut();
246    match group_id {
247        Some(group_id) => set_tellers_group_id(metadata, group_id),
248        None => {
249            remove_tellers_group_id(metadata);
250        }
251    }
252}
253
254/// Read the Tellers group id from `metadata["tellers.ai"]["Tellers Group ID"]`.
255///
256/// This is the Tellers-native grouping concept, kept separate from the Resolve
257/// "Link Group ID" (sync clips) and stored in the Tellers metadata namespace
258/// alongside `timeline_id`. An int, uint, or stringified value all read back.
259pub fn resolve_tellers_group_id(metadata: &serde_json::Value) -> Option<i64> {
260    let raw = metadata
261        .get("tellers.ai")
262        .and_then(|v| v.get("Tellers Group ID"))?;
263    raw.as_i64()
264        .or_else(|| raw.as_u64().and_then(|value| i64::try_from(value).ok()))
265        .or_else(|| raw.as_str().and_then(|value| value.parse::<i64>().ok()))
266}
267
268/// Write `group_id` to `metadata["tellers.ai"]["Tellers Group ID"]`, creating
269/// the `tellers.ai` object if it is missing or not an object.
270pub fn set_tellers_group_id(metadata: &mut serde_json::Value, group_id: i64) {
271    if metadata.as_object().is_none() {
272        *metadata = serde_json::Value::Object(serde_json::Map::new());
273    }
274    let map = metadata.as_object_mut().unwrap();
275    let ai = map
276        .entry("tellers.ai".to_string())
277        .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
278    if ai.as_object().is_none() {
279        *ai = serde_json::Value::Object(serde_json::Map::new());
280    }
281    ai.as_object_mut().unwrap().insert(
282        "Tellers Group ID".to_string(),
283        serde_json::Value::Number(serde_json::Number::from(group_id)),
284    );
285}
286
287/// Remove the Tellers Group ID from `metadata`, returning whether one was
288/// present.
289pub fn remove_tellers_group_id(metadata: &mut serde_json::Value) -> bool {
290    let Some(ai) = metadata
291        .get_mut("tellers.ai")
292        .and_then(|value| value.as_object_mut())
293    else {
294        return false;
295    };
296    ai.remove("Tellers Group ID").is_some()
297}