1use crate::{ProxyGenerationSettings, ProxyOptimizer, Result};
4use std::path::Path;
5
6pub struct WorkflowPlanner {
8 optimizer: ProxyOptimizer,
9}
10
11impl WorkflowPlanner {
12 #[must_use]
14 pub fn new() -> Self {
15 Self {
16 optimizer: ProxyOptimizer::new(),
17 }
18 }
19
20 pub fn plan_generation(
22 &self,
23 inputs: &[MediaInfo],
24 settings: ProxyGenerationSettings,
25 ) -> Result<WorkflowPlan> {
26 let total_duration: f64 = inputs.iter().map(|m| m.duration).sum();
27 let total_input_size: u64 = inputs.iter().map(|m| m.file_size).sum();
28
29 let mut estimated_output_size = 0u64;
30 let mut estimated_encoding_time = 0.0;
31
32 for media in inputs {
33 let output_size = self
34 .optimizer
35 .estimate_output_size(&settings, media.duration);
36 let encoding_time = self
37 .optimizer
38 .estimate_encoding_time(&settings, media.duration);
39
40 estimated_output_size += output_size;
41 estimated_encoding_time += encoding_time;
42 }
43
44 let space_savings = if total_input_size > estimated_output_size {
45 total_input_size - estimated_output_size
46 } else {
47 0
48 };
49
50 let compression_ratio = if total_input_size > 0 {
51 estimated_output_size as f64 / total_input_size as f64
52 } else {
53 0.0
54 };
55
56 Ok(WorkflowPlan {
57 total_files: inputs.len(),
58 total_duration,
59 total_input_size,
60 estimated_output_size,
61 estimated_encoding_time,
62 space_savings,
63 compression_ratio,
64 recommended_parallel_jobs: calculate_recommended_jobs(inputs.len()),
65 })
66 }
67
68 pub fn plan_offline_workflow(
70 &self,
71 inputs: &[MediaInfo],
72 settings: ProxyGenerationSettings,
73 estimated_editing_time: f64,
74 ) -> Result<OfflineWorkflowPlan> {
75 let generation_plan = self.plan_generation(inputs, settings)?;
76 let encoding_time = generation_plan.estimated_encoding_time;
77
78 let estimated_conform_time = encoding_time * 0.1;
80
81 let total_time = encoding_time + estimated_editing_time + estimated_conform_time;
83
84 Ok(OfflineWorkflowPlan {
85 generation_plan,
86 estimated_editing_time,
87 estimated_conform_time,
88 total_workflow_time: total_time,
89 phases: vec![
90 WorkflowPhase {
91 name: "Proxy Generation".to_string(),
92 estimated_time: encoding_time,
93 },
94 WorkflowPhase {
95 name: "Offline Editing".to_string(),
96 estimated_time: estimated_editing_time,
97 },
98 WorkflowPhase {
99 name: "Conforming".to_string(),
100 estimated_time: estimated_conform_time,
101 },
102 ],
103 })
104 }
105
106 #[must_use]
108 pub fn estimate_storage(
109 &self,
110 inputs: &[MediaInfo],
111 settings: &ProxyGenerationSettings,
112 keep_originals: bool,
113 ) -> StorageEstimate {
114 let total_original_size: u64 = inputs.iter().map(|m| m.file_size).sum();
115
116 let total_proxy_size: u64 = inputs
117 .iter()
118 .map(|m| self.optimizer.estimate_output_size(settings, m.duration))
119 .sum();
120
121 let working_storage = if keep_originals {
122 total_original_size + total_proxy_size
123 } else {
124 total_proxy_size
125 };
126
127 let recommended_storage = (working_storage as f64 * 1.2) as u64;
129
130 StorageEstimate {
131 original_size: total_original_size,
132 proxy_size: total_proxy_size,
133 working_storage,
134 recommended_storage,
135 space_saved: if total_original_size > total_proxy_size {
136 total_original_size - total_proxy_size
137 } else {
138 0
139 },
140 }
141 }
142}
143
144impl Default for WorkflowPlanner {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct MediaInfo {
153 pub path: std::path::PathBuf,
155
156 pub file_size: u64,
158
159 pub duration: f64,
161
162 pub resolution: (u32, u32),
164
165 pub frame_rate: f64,
167}
168
169impl MediaInfo {
170 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
176 let path = path.as_ref();
177
178 let metadata = std::fs::metadata(path)?;
179 let file_size = metadata.len();
180
181 Ok(Self {
183 path: path.to_path_buf(),
184 file_size,
185 duration: 0.0,
186 resolution: (0, 0),
187 frame_rate: 0.0,
188 })
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct WorkflowPlan {
195 pub total_files: usize,
197
198 pub total_duration: f64,
200
201 pub total_input_size: u64,
203
204 pub estimated_output_size: u64,
206
207 pub estimated_encoding_time: f64,
209
210 pub space_savings: u64,
212
213 pub compression_ratio: f64,
215
216 pub recommended_parallel_jobs: usize,
218}
219
220impl WorkflowPlan {
221 #[must_use]
223 pub fn summary(&self) -> String {
224 format!(
225 "Workflow Plan:\n\
226 Files: {}\n\
227 Total Duration: {:.1} hours\n\
228 Input Size: {}\n\
229 Estimated Output: {}\n\
230 Space Savings: {} ({:.1}%)\n\
231 Estimated Time: {}\n\
232 Recommended Parallel Jobs: {}",
233 self.total_files,
234 self.total_duration / 3600.0,
235 format_bytes(self.total_input_size),
236 format_bytes(self.estimated_output_size),
237 format_bytes(self.space_savings),
238 (1.0 - self.compression_ratio) * 100.0,
239 format_duration(self.estimated_encoding_time),
240 self.recommended_parallel_jobs
241 )
242 }
243}
244
245#[derive(Debug, Clone)]
247pub struct OfflineWorkflowPlan {
248 pub generation_plan: WorkflowPlan,
250
251 pub estimated_editing_time: f64,
253
254 pub estimated_conform_time: f64,
256
257 pub total_workflow_time: f64,
259
260 pub phases: Vec<WorkflowPhase>,
262}
263
264#[derive(Debug, Clone)]
266pub struct WorkflowPhase {
267 pub name: String,
269
270 pub estimated_time: f64,
272}
273
274#[derive(Debug, Clone)]
276pub struct StorageEstimate {
277 pub original_size: u64,
279
280 pub proxy_size: u64,
282
283 pub working_storage: u64,
285
286 pub recommended_storage: u64,
288
289 pub space_saved: u64,
291}
292
293fn calculate_recommended_jobs(total_files: usize) -> usize {
294 let cpu_count = std::thread::available_parallelism()
295 .map(|n| n.get())
296 .unwrap_or(1);
297
298 (cpu_count / 2).max(1).min(total_files)
300}
301
302fn format_bytes(bytes: u64) -> String {
303 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
304 let mut size = bytes as f64;
305 let mut unit_index = 0;
306
307 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
308 size /= 1024.0;
309 unit_index += 1;
310 }
311
312 format!("{:.2} {}", size, UNITS[unit_index])
313}
314
315fn format_duration(seconds: f64) -> String {
316 let hours = (seconds / 3600.0).floor() as u64;
317 let minutes = ((seconds % 3600.0) / 60.0).floor() as u64;
318 let secs = (seconds % 60.0).floor() as u64;
319
320 if hours > 0 {
321 format!("{}h {}m {}s", hours, minutes, secs)
322 } else if minutes > 0 {
323 format!("{}m {}s", minutes, secs)
324 } else {
325 format!("{}s", secs)
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_workflow_planner() {
335 let planner = WorkflowPlanner::new();
336
337 let inputs = vec![
338 MediaInfo {
339 path: "test1.mov".into(),
340 file_size: 1_000_000_000,
341 duration: 60.0,
342 resolution: (1920, 1080),
343 frame_rate: 25.0,
344 },
345 MediaInfo {
346 path: "test2.mov".into(),
347 file_size: 1_000_000_000,
348 duration: 60.0,
349 resolution: (1920, 1080),
350 frame_rate: 25.0,
351 },
352 ];
353
354 let settings = ProxyGenerationSettings::quarter_res_h264();
355 let plan = planner
356 .plan_generation(&inputs, settings)
357 .expect("should succeed in test");
358
359 assert_eq!(plan.total_files, 2);
360 assert_eq!(plan.total_duration, 120.0);
361 assert!(plan.estimated_output_size > 0);
362 assert!(plan.estimated_encoding_time > 0.0);
363 }
364
365 #[test]
366 fn test_offline_workflow_plan() {
367 let planner = WorkflowPlanner::new();
368
369 let inputs = vec![MediaInfo {
370 path: "test.mov".into(),
371 file_size: 1_000_000_000,
372 duration: 600.0,
373 resolution: (1920, 1080),
374 frame_rate: 25.0,
375 }];
376
377 let settings = ProxyGenerationSettings::quarter_res_h264();
378 let plan = planner
379 .plan_offline_workflow(&inputs, settings, 7200.0)
380 .expect("should succeed in test");
381
382 assert_eq!(plan.phases.len(), 3);
383 assert!(plan.total_workflow_time > 0.0);
384 }
385
386 #[test]
387 fn test_storage_estimate() {
388 let planner = WorkflowPlanner::new();
389
390 let inputs = vec![MediaInfo {
391 path: "test.mov".into(),
392 file_size: 1_000_000_000,
393 duration: 600.0,
394 resolution: (1920, 1080),
395 frame_rate: 25.0,
396 }];
397
398 let settings = ProxyGenerationSettings::quarter_res_h264();
399 let estimate = planner.estimate_storage(&inputs, &settings, true);
400
401 assert_eq!(estimate.original_size, 1_000_000_000);
402 assert!(estimate.proxy_size > 0);
403 assert!(estimate.working_storage > 0);
404 }
405
406 #[test]
407 fn test_format_duration() {
408 assert_eq!(format_duration(30.0), "30s");
409 assert_eq!(format_duration(90.0), "1m 30s");
410 assert_eq!(format_duration(3665.0), "1h 1m 5s");
411 }
412
413 #[test]
414 fn test_calculate_recommended_jobs() {
415 let jobs = calculate_recommended_jobs(10);
416 assert!(jobs > 0);
417 assert!(jobs <= 10);
418 }
419}