1use std::collections::BTreeMap;
10
11use alpha2_model::MediaFingerprint;
12use alpha2_time::FrameRate;
13use serde::{Deserialize, Serialize};
14
15use crate::spec::{FileKind, MediaItem, Node, VideoSpec};
16
17#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[serde(deny_unknown_fields)]
22pub struct Facts {
23 #[serde(default)]
25 pub media: BTreeMap<String, MediaFacts>,
26 #[serde(default)]
28 pub texts: BTreeMap<String, String>,
29}
30
31#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[serde(deny_unknown_fields)]
35pub struct MediaFacts {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub width: Option<u32>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub height: Option<u32>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub frame_rate: Option<FrameRate>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub duration_ticks: Option<i64>,
48 #[serde(default)]
50 pub has_audio: bool,
51}
52
53impl MediaFacts {
54 #[must_use]
57 pub fn from_fingerprint(fingerprint: &MediaFingerprint) -> Self {
58 let streams = fingerprint.streams.as_ref();
59 let video = streams.and_then(|streams| streams.video.as_ref());
60 Self {
61 width: video.map(|video| video.width),
62 height: video.map(|video| video.height),
63 frame_rate: video.and_then(|video| video.frame_rate),
64 duration_ticks: fingerprint.duration_ticks,
65 has_audio: streams.is_some_and(|streams| streams.audio.is_some()),
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(deny_unknown_fields)]
74pub struct Requirements {
75 pub media: Vec<RequiredMedia>,
77 pub texts: Vec<String>,
79}
80
81#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(deny_unknown_fields)]
85pub struct RequiredMedia {
86 pub path: String,
88 pub kind: FileKind,
90}
91
92#[must_use]
97pub fn kind_of_path(path: &str) -> FileKind {
98 let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
99 let extension = name
100 .rsplit_once('.')
101 .map(|(_, extension)| extension.to_ascii_lowercase())
102 .unwrap_or_default();
103 match extension.as_str() {
104 "png" | "jpg" | "jpeg" | "webp" | "bmp" | "tif" | "tiff" | "avif" => FileKind::Image,
105 "mp3" | "m4a" | "aac" | "wav" | "flac" | "ogg" | "oga" | "opus" | "aif" | "aiff"
106 | "caf" => FileKind::Audio,
107 _ => FileKind::Video,
108 }
109}
110
111#[must_use]
113pub fn requirements(spec: &VideoSpec) -> Requirements {
114 let mut found = Requirements::default();
115 let nodes = std::iter::once(&spec.body)
116 .chain(&spec.overlays)
117 .chain(&spec.audio);
118 for node in nodes {
119 walk(node, &mut found);
120 }
121 found
122}
123
124fn add_media(found: &mut Requirements, path: &str, kind: Option<FileKind>) {
125 if !found.media.iter().any(|media| media.path == path) {
126 found.media.push(RequiredMedia {
127 path: path.to_owned(),
128 kind: kind.unwrap_or_else(|| kind_of_path(path)),
129 });
130 }
131}
132
133fn add_item(found: &mut Requirements, item: &MediaItem) {
134 match item {
135 MediaItem::Path(path) => add_media(found, path, None),
136 MediaItem::Node(node) => walk(node, found),
137 }
138}
139
140fn walk(node: &Node, found: &mut Requirements) {
141 match node {
142 Node::Sequence(sequence) => sequence
143 .children
144 .iter()
145 .for_each(|child| walk(child, found)),
146 Node::Stack(stack) => stack.children.iter().for_each(|child| walk(child, found)),
147 Node::Grid(grid) => grid.children.iter().for_each(|child| walk(child, found)),
148 Node::Media(media) => add_media(found, &media.path, media.kind),
149 Node::Audio(audio) => add_media(found, &audio.path, Some(FileKind::Audio)),
150 Node::MusicBed(music) => add_media(found, &music.path, Some(FileKind::Audio)),
151 Node::Logo(logo) => add_media(found, &logo.path, None),
152 Node::Captions(captions) => {
153 if let Some(file) = &captions.file
154 && !found.texts.contains(file)
155 {
156 found.texts.push(file.clone());
157 }
158 }
159 Node::Slideshow(slideshow) => {
160 slideshow
161 .items
162 .iter()
163 .for_each(|item| add_item(found, item));
164 if let Some(path) = &slideshow.fit_to {
165 add_media(found, path, Some(FileKind::Audio));
166 }
167 }
168 Node::CaptionedVideo(captioned) => {
169 add_item(found, &captioned.video);
170 walk(&Node::Captions((*captioned.captions).clone()), found);
171 }
172 Node::ComparisonGrid(grid) => grid.items.iter().for_each(|item| add_item(found, item)),
173 Node::Reframe(reframe) => add_item(found, &reframe.video),
174 Node::EndCard(card) => {
175 if let Some(logo) = &card.logo {
176 add_media(found, logo, None);
177 }
178 }
179 Node::Text(_)
180 | Node::Color(_)
181 | Node::Layout(_)
182 | Node::Raw(_)
183 | Node::Title(_)
184 | Node::TitleCard(_)
185 | Node::LowerThird(_) => {}
186 }
187}