225 lines
6.3 KiB
Rust
225 lines
6.3 KiB
Rust
![]() |
use std::{path::{PathBuf, Path}, process::Command};
|
||
|
|
||
|
use serde::Deserialize;
|
||
|
use string_error::static_err;
|
||
|
|
||
|
use crate::types::{AudioFileInfo, ReplayGainData, ReplayGainRawData, Tags};
|
||
|
|
||
|
use super::{AudioContainer, 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 {
|
||
|
container_type: AudioContainer,
|
||
|
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 get_replaygain_data(&self) -> Option<ReplayGainData> {
|
||
|
if let Some(data) = &self.changes.replaygain_data {
|
||
|
return Some(data.to_normal());
|
||
|
}
|
||
|
|
||
|
self.extracted_data.replaygain_data.clone()
|
||
|
}
|
||
|
|
||
|
fn supports_replaygain(&self) -> bool {
|
||
|
match self.container_type {
|
||
|
AudioContainer::MP3 => true,
|
||
|
AudioContainer::FLAC => true,
|
||
|
AudioContainer::WAV => true,
|
||
|
AudioContainer::OGG => false, // ffprobe can't do OGG tags
|
||
|
AudioContainer::AIFF => true,
|
||
|
AudioContainer::Unknown => 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(&self, allow_missing_tags: bool) -> Result<AudioFileInfo, BoxedError> {
|
||
|
return Ok(AudioFileInfo {
|
||
|
tags: self.get_tags(allow_missing_tags)?,
|
||
|
replaygain: self.get_replaygain_data(),
|
||
|
supports_replaygain: self.supports_replaygain(),
|
||
|
container: self.container_type,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn new_generic_ffmpeg_format_handler(
|
||
|
path: &Path,
|
||
|
container_type: AudioContainer,
|
||
|
) -> Result<GenericFFMpegAudioFormat, BoxedError> {
|
||
|
let mut handler = GenericFFMpegAudioFormat {
|
||
|
container_type,
|
||
|
path: Box::new(path.to_path_buf()),
|
||
|
extracted_data: ExtractedData::default(),
|
||
|
changes: Changes::default(),
|
||
|
};
|
||
|
handler.analyze()?;
|
||
|
|
||
|
Ok(handler)
|
||
|
}
|