musicutil/src/utils/formats/generic_ffmpeg.rs

231 lines
6 KiB
Rust
Raw Normal View History

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<String>,
artist: Option<String>,
replaygain_data: Option<ReplayGainRawData>,
}
#[derive(Default)]
struct ExtractedData {
tags: Tags,
replaygain_data: Option<ReplayGainData>,
}
#[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<String>,
#[serde(default, alias = "REPLAYGAIN_TRACK_GAIN")]
pub replaygain_track_gain: Option<String>,
}
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<PathBuf>,
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<Tags, BoxedError> {
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<String> = 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<AudioFileInfo, BoxedError> {
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<GenericFFMpegAudioFormat, BoxedError> {
let mut handler = GenericFFMpegAudioFormat {
format_type,
path: Box::new(path.to_path_buf()),
extracted_data: ExtractedData::default(),
changes: Changes::default(),
};
handler.analyze()?;
Ok(handler)
}