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