use std::path::PathBuf; use anyhow::{Result, anyhow}; use chrono::NaiveDate; use regex::Regex; use video_processing::{ VideoProcessingConfig, VideoConverter as SharedVideoConverter, NfoGenerator, FileProcessor }; pub struct VideoConverter { output_path: PathBuf, config: VideoProcessingConfig, video_converter: SharedVideoConverter, nfo_generator: NfoGenerator, } impl VideoConverter { pub fn new(output_path: PathBuf) -> Self { let config = VideoProcessingConfig::from_env(); let video_converter = SharedVideoConverter::new(config.clone()); let nfo_generator = NfoGenerator::new(config.clone()); VideoConverter { output_path, config, video_converter, nfo_generator, } } // Note: File stability checking is now handled by the caller using the shared crate pub async fn extract_date_from_filename(&self, filename: &str) -> Result { // Example filename: "Sermon Title - Speaker (December 4th 2024).mp4" if let Some(date_part) = filename.split('(').nth(1).and_then(|part| part.split(')').next()) { let re = Regex::new(r"(?i)(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d+)(?:st|nd|rd|th)?\s+(\d{4})")?; if let Some(caps) = re.captures(date_part) { let month = caps.get(1).unwrap().as_str(); let day = caps.get(2).unwrap().as_str(); let year = caps.get(3).unwrap().as_str(); let date_str = format!("{} {} {}", day, month, year); let date = NaiveDate::parse_from_str(&date_str, "%d %B %Y")?; Ok(date) } else { Err(anyhow!("Could not extract date from filename part within parentheses")) } } else { Err(anyhow!("No date found in parentheses in filename")) } } pub async fn extract_title_and_speaker(&self, filename: &str) -> Result<(String, String)> { // Example: "God's People in the Time of the End - Pastor Joseph Piresson (December 28th 2024).mp4" if let Some(main_part) = filename.split('(').next() { if let Some((title, speaker)) = main_part.split_once('-') { return Ok(( title.trim().to_string(), speaker.trim().to_string() )); } } Err(anyhow!("Could not extract title and speaker from filename")) } pub async fn process_file(&self, path: PathBuf) -> Result<()> { // Early checks if path.to_string_lossy().contains(".syncthing.") { return Err(anyhow!("Skipping Syncthing temporary file")); } if path.extension().and_then(|ext| ext.to_str()) != Some("mp4") { return Err(anyhow!("Ignoring non-MP4 file")); } if !path.exists() { return Err(anyhow!("File no longer exists: {}", path.display())); } // Note: File stability is now handled by the caller println!("Processing sermon: {}", path.display()); // Get the filename let filename = path.file_name() .ok_or_else(|| anyhow!("Invalid filename"))? .to_str() .ok_or_else(|| anyhow!("Invalid UTF-8 in filename"))?; // Extract date, title, and speaker from filename let date = self.extract_date_from_filename(filename).await?; let (title, speaker) = self.extract_title_and_speaker(filename).await?; // Create date-based directory structure let year_dir = self.output_path.join(date.format("%Y").to_string()); let month_dir = year_dir.join(format!("{}-{}", date.format("%m"), // numeric month (12) date.format("%B") // full month name (December) )); // Create directories if they don't exist tokio::fs::create_dir_all(&month_dir).await?; // Keep original filename but ensure .mp4 extension let output_file = month_dir.join(filename); println!("Converting to AV1 and saving to: {}", output_file.display()); // Convert video using shared crate self.video_converter.convert_video(&path, &output_file).await?; // Create NFO file using shared crate let full_title = format!("{} - {}", title, speaker); self.nfo_generator.create_nfo_file(&output_file, &full_title, &date, None).await?; println!("Successfully converted {} to AV1 and created NFO", path.display()); // Handle original file based on configuration if !self.config.preserve_original_files { println!("Deleting original file: {}", path.display()); tokio::fs::remove_file(path).await?; } else { println!("Preserving original file: {}", path.display()); } Ok(()) } } #[async_trait::async_trait] impl FileProcessor for VideoConverter { async fn process_file(&self, path: PathBuf) -> Result<()> { self.process_file(path).await } async fn should_skip_existing_file(&self, _path: &PathBuf) -> bool { // For sermons, we don't have a simple duplicate check like livestreams // Let the process_file method handle duplicate detection false } }