use std::{ path::{Path, PathBuf}, process::Command, }; use serde::Deserialize; use string_error::static_err; use crate::{ types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags}, utils::format_detection::FileFormat, }; use super::{AudioContainerFormat, AudioFormatError, BoxedError}; #[derive(Default)] struct Changes { title: Option, artist: Option, replaygain_data: Option, } #[derive(Default)] struct ExtractedData { tags: Tags, replaygain_data: Option, } #[derive(Debug, Clone, Deserialize)] struct FFProbeOutput { pub format: FFProbeFormat, } #[derive(Debug, Clone, Deserialize)] struct FFProbeFormat { pub tags: FFProbeTags, } #[derive(Debug, Clone, Deserialize)] struct FFProbeTags { #[serde(alias = "TITLE")] pub title: String, #[serde(default, alias = "ARTIST")] pub artist: String, #[serde(default, alias = "REPLAYGAIN_TRACK_PEAK")] pub replaygain_track_peak: Option, #[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")] pub replaygain_track_gain: Option, } impl Default for FFProbeTags { fn default() -> Self { FFProbeTags { title: "".to_string(), artist: "".to_string(), replaygain_track_peak: None, replaygain_track_gain: None, } } } pub struct GenericFFMpegAudioFormat { format_type: FileFormat, path: Box, extracted_data: ExtractedData, changes: Changes, } impl GenericFFMpegAudioFormat { fn analyze(&mut self) -> Result<(), BoxedError> { let output = Command::new(crate::meta::FFPROBE) .args([ "-v", "quiet", "-print_format", "json", "-show_format", &self.path.to_string_lossy(), ]) .output()?; if !output.status.success() { print!("{:?}", String::from_utf8(output.stderr).unwrap()); return Err(static_err("FFprobe Crashed")); } let output_str = String::from_utf8(output.stdout).unwrap(); let ffprobe_out: FFProbeOutput = serde_json::from_str(output_str.as_str())?; let tags = ffprobe_out.format.tags; self.extracted_data.tags = Tags { title: tags.title, artist: tags.artist, }; if tags.replaygain_track_gain.is_some() && tags.replaygain_track_peak.is_some() { self.extracted_data.replaygain_data = Some(ReplayGainData { track_gain: tags.replaygain_track_gain.unwrap(), track_peak: tags.replaygain_track_peak.unwrap(), }); } else { self.extracted_data.replaygain_data = None; } Ok(()) } } impl AudioContainerFormat for GenericFFMpegAudioFormat { fn get_tags(&self, allow_missing: bool) -> Result { let mut tags = self.extracted_data.tags.clone(); if let Some(title) = &self.changes.title { tags.title = title.clone(); } if let Some(artist) = &self.changes.title { tags.artist = artist.clone(); } if !allow_missing { if tags.title.is_empty() { return Err(Box::new(AudioFormatError::MissingTitle)); } if tags.artist.is_empty() { return Err(Box::new(AudioFormatError::MissingArtist)); } } Ok(tags) } fn contains_replaygain_tags(&self) -> bool { if self.changes.replaygain_data.is_some() { return true; } if self.extracted_data.replaygain_data.is_some() { return true; }; false } fn supports_replaygain(&self) -> bool { false } fn set_title(&mut self, title: String) -> Result<(), BoxedError> { self.changes.title = Some(title); Ok(()) } fn set_artist(&mut self, artist: String) -> Result<(), BoxedError> { self.changes.artist = Some(artist); Ok(()) } fn set_replaygain_data(&mut self, data: ReplayGainRawData) -> Result<(), BoxedError> { self.changes.replaygain_data = Some(data); Ok(()) } fn save_changes(&mut self) -> Result<(), BoxedError> { let mut args: Vec = Vec::new(); let tempdir = tempfile::tempdir()?; let temp_file = tempdir .path() .join(PathBuf::from(self.path.file_name().unwrap())); args.extend(vec![ "-i".to_string(), self.path.to_string_lossy().to_string(), ]); args.extend(vec!["-c".to_string(), "copy".to_string()]); if let Some(title) = self.changes.title.clone() { args.extend(vec![ "-metadata".to_string(), format!("title=\"{}\"", title), ]) } if let Some(artist) = self.changes.artist.clone() { args.extend(vec!["-metadata".to_string(), format!("artist={}", artist)]) } args.push(temp_file.to_string_lossy().to_string()); let output = Command::new(crate::meta::FFMPEG).args(args).output()?; println!("{:?}", String::from_utf8(output.stderr)); std::fs::copy(temp_file, self.path.to_path_buf())?; Ok(()) } fn get_audio_file_info( &mut self, allow_missing_tags: bool, ) -> Result { return Ok(AudioFileInfo { tags: self.get_tags(allow_missing_tags)?, contains_replaygain: self.contains_replaygain_tags(), supports_replaygain: self.supports_replaygain(), format: Some(self.format_type), }); } } pub fn new_generic_ffmpeg_format_handler( path: &Path, format_type: FileFormat, ) -> Result { let mut handler = GenericFFMpegAudioFormat { format_type, path: Box::new(path.to_path_buf()), extracted_data: ExtractedData::default(), changes: Changes::default(), }; handler.analyze()?; Ok(handler) }