alpha2_model/
fingerprint.rs1use alpha2_media::ColorMetadata;
5use alpha2_time::FrameRate;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[serde(deny_unknown_fields)]
16pub struct MediaFingerprint {
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub content_sha256: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub byte_length: Option<u64>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub modified_ms: Option<i64>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub streams: Option<StreamTopology>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub duration_ticks: Option<i64>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub color: Option<ColorMetadata>,
38}
39
40impl MediaFingerprint {
41 #[must_use]
43 pub fn is_valid(&self) -> bool {
44 self.malformed_part().is_none()
45 }
46
47 #[must_use]
52 pub fn malformed_part(&self) -> Option<&'static str> {
53 if self
54 .content_sha256
55 .as_deref()
56 .is_some_and(|hash| !is_sha256_hex(hash))
57 {
58 Some("content_sha256")
59 } else if self.duration_ticks.is_some_and(|ticks| ticks < 0) {
60 Some("duration_ticks")
61 } else if self
62 .streams
63 .as_ref()
64 .is_some_and(|streams| !streams.is_valid())
65 {
66 Some("streams")
67 } else if self.color.is_some_and(|color| !color.is_consistent()) {
68 Some("color")
69 } else {
70 None
71 }
72 }
73}
74
75#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[serde(deny_unknown_fields)]
79pub struct StreamTopology {
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub video: Option<VideoStreamShape>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub audio: Option<AudioStreamShape>,
86}
87
88impl StreamTopology {
89 fn is_valid(&self) -> bool {
90 self.video
91 .as_ref()
92 .is_none_or(|video| video.width > 0 && video.height > 0)
93 && self
94 .audio
95 .as_ref()
96 .is_none_or(|audio| audio.channels > 0 && audio.sample_rate > 0)
97 }
98}
99
100#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103#[serde(deny_unknown_fields)]
104#[non_exhaustive]
105pub struct VideoStreamShape {
106 pub width: u32,
110 pub height: u32,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub frame_rate: Option<FrameRate>,
119}
120
121impl VideoStreamShape {
122 #[must_use]
124 pub const fn new(width: u32, height: u32, frame_rate: Option<FrameRate>) -> Self {
125 Self {
126 width,
127 height,
128 frame_rate,
129 }
130 }
131}
132
133#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[serde(deny_unknown_fields)]
137#[non_exhaustive]
138pub struct AudioStreamShape {
139 pub channels: u32,
141 pub sample_rate: u32,
143}
144
145impl AudioStreamShape {
146 #[must_use]
148 pub const fn new(channels: u32, sample_rate: u32) -> Self {
149 Self {
150 channels,
151 sample_rate,
152 }
153 }
154}
155
156fn is_sha256_hex(hash: &str) -> bool {
157 hash.len() == 64
158 && hash
159 .bytes()
160 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
161}