1use serde::Serialize;
21use serde::de::DeserializeOwned;
22use serde_json::{Map, Value};
23
24use super::schema::{
25 Clip, Effect, ExternalReference, Gap, GeneratorReference, GenericEffect, Item, LinearTimeWarp,
26 Marker, MediaReference, MissingReference, Stack, Timeline, Track, Transition,
27};
28use ffcine_core::{Error, Result};
29
30pub const FFCINE: &str = "ffcine";
32pub const FFCINE_VERSION: u64 = 1;
34
35pub trait HasMetadata {
37 fn metadata(&self) -> &Map<String, Value>;
38 fn metadata_mut(&mut self) -> &mut Map<String, Value>;
39
40 fn ffcine(&self) -> Option<&Map<String, Value>> {
42 self.metadata().get(FFCINE).and_then(Value::as_object)
43 }
44
45 fn ffcine_mut(&mut self) -> &mut Map<String, Value> {
47 object_entry(self.metadata_mut(), FFCINE)
48 }
49
50 fn ffcine_value(&self, key: &str) -> Option<&Value> {
52 self.ffcine()?.get(key)
53 }
54
55 fn ffcine_get<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T>> {
57 self.ffcine_value(key)
58 .map(|v| T::deserialize(v).map_err(Error::from))
59 }
60
61 fn ffcine_set<T: Serialize + ?Sized>(&mut self, key: &str, value: &T) -> Result<()> {
63 let v = serde_json::to_value(value)?;
64 self.ffcine_mut().insert(key.to_owned(), v);
65 Ok(())
66 }
67
68 fn ffcine_id(&self) -> Option<&str> {
70 self.ffcine_value("id").and_then(Value::as_str)
71 }
72
73 fn set_ffcine_id(&mut self, id: impl Into<String>) {
74 self.ffcine_mut()
75 .insert("id".into(), Value::String(id.into()));
76 }
77
78 fn foreign(&self, app: &str) -> Option<&Value> {
80 self.ffcine_value("foreign")?.as_object()?.get(app)
81 }
82
83 fn set_foreign(&mut self, app: &str, value: Value) {
85 object_entry(self.ffcine_mut(), "foreign").insert(app.to_owned(), value);
86 }
87
88 fn insert_foreign(&mut self, app: &str, key: &str, value: Value) {
90 let foreign = object_entry(self.ffcine_mut(), "foreign");
91 object_entry(foreign, app).insert(key.to_owned(), value);
92 }
93
94 fn move_to_foreign(&mut self, key: &str, app: &str) -> bool {
97 match self.metadata_mut().shift_remove(key) {
98 Some(v) => {
99 self.set_foreign(app, v);
100 true
101 }
102 None => false,
103 }
104 }
105}
106
107fn object_entry<'a>(map: &'a mut Map<String, Value>, key: &str) -> &'a mut Map<String, Value> {
109 let slot = map.entry(key).or_insert_with(|| Value::Object(Map::new()));
110 if !slot.is_object() {
111 *slot = Value::Object(Map::new());
112 }
113 match slot {
114 Value::Object(m) => m,
115 _ => unreachable!("just made an object"),
116 }
117}
118
119macro_rules! has_metadata {
120 ($($t:ty),*) => {$(
121 impl HasMetadata for $t {
122 fn metadata(&self) -> &Map<String, Value> {
123 &self.metadata
124 }
125 fn metadata_mut(&mut self) -> &mut Map<String, Value> {
126 &mut self.metadata
127 }
128 }
129 )*};
130}
131
132has_metadata!(
133 Timeline,
134 Stack,
135 Track,
136 Clip,
137 Gap,
138 Transition,
139 GenericEffect,
140 LinearTimeWarp,
141 Marker,
142 ExternalReference,
143 MissingReference,
144 GeneratorReference
145);
146
147static EMPTY: std::sync::LazyLock<Map<String, Value>> = std::sync::LazyLock::new(Map::new);
149
150macro_rules! has_metadata_enum {
152 ($t:ident { $($v:ident),* }) => {
153 impl HasMetadata for $t {
154 fn metadata(&self) -> &Map<String, Value> {
155 match self {
156 $($t::$v(x) => &x.metadata,)*
157 $t::Unknown(m) => m.get("metadata").and_then(Value::as_object).unwrap_or(&EMPTY),
158 }
159 }
160 fn metadata_mut(&mut self) -> &mut Map<String, Value> {
161 match self {
162 $($t::$v(x) => &mut x.metadata,)*
163 $t::Unknown(m) => object_entry(m, "metadata"),
164 }
165 }
166 }
167 };
168}
169
170has_metadata_enum!(Item {
171 Clip,
172 Gap,
173 Stack,
174 Track,
175 Transition
176});
177has_metadata_enum!(Effect {
178 Effect,
179 LinearTimeWarp,
180 FreezeFrame
181});
182has_metadata_enum!(MediaReference {
183 External,
184 Missing,
185 Generator
186});
187
188pub fn split_ffcine_schema(s: &str) -> Option<(&str, u32)> {
190 let (name, major) = s.rsplit_once('/')?;
191 Some((name, major.parse().ok()?))
192}
193
194impl GenericEffect {
195 pub fn ffcine(schema: &str, params: Map<String, Value>) -> Self {
198 let effect_name = split_ffcine_schema(schema).map_or(schema, |(n, _)| n);
199 let short = effect_name.rsplit('.').next().unwrap_or(effect_name);
200 let mut e = GenericEffect::new(short, effect_name);
201 let ns = e.ffcine_mut();
202 ns.insert("schema".into(), schema.into());
203 ns.extend(params);
204 e
205 }
206
207 pub fn ffcine_schema(&self) -> Option<(&str, u32)> {
209 self.ffcine_value("schema")
210 .and_then(Value::as_str)
211 .and_then(split_ffcine_schema)
212 }
213
214 pub fn ffcine_params<T: DeserializeOwned>(&self, name: &str, major: u32) -> Option<Result<T>> {
216 if self.ffcine_schema() != Some((name, major)) {
217 return None;
218 }
219 let mut params = self.ffcine()?.clone();
220 params.shift_remove("schema");
221 Some(T::deserialize(Value::Object(params)).map_err(Error::from))
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use serde_json::json;
229
230 #[test]
231 fn foreign_and_id() {
232 let mut t = Timeline::default();
233 t.metadata.insert(
234 "Resolve_OTIO".into(),
235 json!({"Resolve OTIO Meta Version": "1.0"}),
236 );
237 assert!(t.move_to_foreign("Resolve_OTIO", "resolve"));
238 t.set_ffcine_id("a");
239 t.insert_foreign("edit_yaml", "audio", json!("nat"));
240 assert_eq!(
241 Value::Object(t.metadata.clone()),
242 json!({"ffcine": {
243 "foreign": {
244 "resolve": {"Resolve OTIO Meta Version": "1.0"},
245 "edit_yaml": {"audio": "nat"},
246 },
247 "id": "a",
248 }})
249 );
250 assert_eq!(t.ffcine_id(), Some("a"));
251 assert_eq!(t.foreign("edit_yaml"), Some(&json!({"audio": "nat"})));
252 }
253
254 #[test]
255 fn ffcine_effect() {
256 let params = json!({"look": "day", "gamma": 1.2});
257 let Value::Object(params) = params else {
258 unreachable!()
259 };
260 let e = GenericEffect::ffcine("ffcine.color.grade/1", params);
261 assert_eq!(e.name, "grade");
262 assert_eq!(e.effect_name, "ffcine.color.grade");
263 assert_eq!(e.ffcine_schema(), Some(("ffcine.color.grade", 1)));
264 let p: Value = e
265 .ffcine_params("ffcine.color.grade", 1)
266 .and_then(Result::ok)
267 .unwrap_or_default();
268 assert_eq!(p, json!({"look": "day", "gamma": 1.2}));
269 assert!(e.ffcine_params::<Value>("ffcine.color.grade", 2).is_none());
270 }
271}