1use std::fmt::{Display, Formatter, Result as FmtResult};
25use std::time::Duration;
26
27use crate::metadata::MediaMetadata;
28
29#[derive(Debug, Clone, Default)]
35pub struct ValidationReport {
36 pub info: Vec<String>,
38 pub warnings: Vec<String>,
40 pub errors: Vec<String>,
42}
43
44impl ValidationReport {
45 pub fn is_valid(&self) -> bool {
50 self.errors.is_empty()
51 }
52
53 pub fn issue_count(&self) -> usize {
55 self.info.len() + self.warnings.len() + self.errors.len()
56 }
57}
58
59impl Display for ValidationReport {
60 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
61 for item in &self.info {
62 writeln!(f, "[INFO] {item}")?;
63 }
64 for item in &self.warnings {
65 writeln!(f, "[WARN] {item}")?;
66 }
67 for item in &self.errors {
68 writeln!(f, "[ERROR] {item}")?;
69 }
70 if self.issue_count() == 0 {
71 writeln!(f, "No issues found.")?;
72 }
73 Ok(())
74 }
75}
76
77pub(crate) fn validate_metadata(metadata: &MediaMetadata) -> ValidationReport {
81 let mut report = ValidationReport::default();
82
83 if metadata.video.is_none() && metadata.audio.is_none() {
85 report
86 .errors
87 .push("File contains neither video nor audio streams".to_string());
88 }
89
90 if metadata.video.is_none() {
91 report.info.push("No video stream found".to_string());
92 }
93
94 if metadata.audio.is_none() {
95 report.info.push("No audio stream found".to_string());
96 }
97
98 if metadata.duration == Duration::ZERO {
100 report
101 .warnings
102 .push("Media duration is zero — frame/time-based extraction may fail".to_string());
103 }
104
105 if let Some(video) = &metadata.video {
107 if video.width == 0 || video.height == 0 {
108 report.errors.push(format!(
109 "Invalid video dimensions: {}×{}",
110 video.width, video.height,
111 ));
112 }
113
114 if video.frames_per_second <= 0.0 {
115 report.warnings.push(
116 "Video frame rate is zero or negative — frame counting will be unreliable"
117 .to_string(),
118 );
119 } else if video.frames_per_second > 240.0 {
120 report.warnings.push(format!(
121 "Unusually high frame rate ({:.1} fps) — extraction may be slow",
122 video.frames_per_second,
123 ));
124 }
125
126 if video.frame_count == 0 && metadata.duration > Duration::ZERO {
127 report
128 .warnings
129 .push("Estimated frame count is zero despite non-zero duration".to_string());
130 }
131
132 report.info.push(format!(
133 "Video: {} {}×{} @ {:.2} fps, ~{} frames",
134 video.codec, video.width, video.height, video.frames_per_second, video.frame_count,
135 ));
136 }
137
138 if let Some(audio) = &metadata.audio {
140 if audio.sample_rate == 0 {
141 report.errors.push("Audio sample rate is zero".to_string());
142 }
143
144 if audio.channels == 0 {
145 report
146 .errors
147 .push("Audio channel count is zero".to_string());
148 }
149
150 report.info.push(format!(
151 "Audio: {} {}Hz {}ch",
152 audio.codec, audio.sample_rate, audio.channels,
153 ));
154 }
155
156 if let Some(tracks) = &metadata.audio_tracks {
158 if tracks.len() > 1 {
159 report
160 .info
161 .push(format!("{} audio tracks available", tracks.len(),));
162 }
163 }
164
165 if let Some(subtitle) = &metadata.subtitle {
167 let language = subtitle.language.as_deref().unwrap_or("unknown language");
168 report
169 .info
170 .push(format!("Subtitle: {} ({})", subtitle.codec, language,));
171 }
172
173 if let Some(tracks) = &metadata.subtitle_tracks {
174 if tracks.len() > 1 {
175 report
176 .info
177 .push(format!("{} subtitle tracks available", tracks.len(),));
178 }
179 }
180
181 report
182}