mod mp3;
use anyhow::Result;
use lofty::{
file::TaggedFileExt,
picture::PictureType,
read_from_path,
tag::{Accessor, ItemKey},
};
use crate::config::RuntimeConfig;
use crate::parsers::media::image;
use crate::parsers::{CompressionMode, FileType, ParseResult, media_helpers};
use crate::results::{AudioMetadata as OutputAudioMetadata, ImageMetadata};
use crate::utils::{
ffprobe_handler::{check_ffprobe_available, run_ffprobe_safe},
filetypes::{get_extensions_for_file_type, is_codec_for_file_type},
};
const LOSSLESS_CODECS: &[&str] = &["flac", "alac", "wavpack", "ape"];
pub(crate) fn is_lossless_codec(codec_ref: &str) -> bool {
let audio_extensions = get_extensions_for_file_type(FileType::Audio);
let codec_lower = codec_ref.to_lowercase();
LOSSLESS_CODECS.iter().any(|lossless_ext| {
codec_lower.contains(lossless_ext) && audio_extensions.contains(lossless_ext)
})
}
#[must_use]
pub fn is_opus_codec(s_ref: &str) -> bool {
is_codec_for_file_type(s_ref, FileType::Audio) && s_ref.to_lowercase().contains("opus")
}
#[derive(Default)]
struct RichTags {
title: Option<String>,
artist: Option<String>,
album: Option<String>,
album_artist: Option<String>,
track: Option<u32>,
track_total: Option<u32>,
year: Option<u32>,
genre: Option<String>,
comments: Option<String>,
artwork: Option<ImageMetadata>,
}
fn extract_compression_mode(codec_ref: &str) -> CompressionMode {
if is_lossless_codec(codec_ref) {
CompressionMode::Lossless
} else {
CompressionMode::Lossy
}
}
pub fn extract_audio_metadata(
_content_ref: &[u8],
stats_ref: &ParseResult,
_config_ref: &RuntimeConfig,
) -> Result<OutputAudioMetadata> {
check_ffprobe_available()?;
let probe_result = run_ffprobe_safe(&stats_ref.file_path)?;
let (container_format, duration_seconds, _bitrate) =
media_helpers::extract_format_metadata(&probe_result);
let audio_stream = media_helpers::find_stream_by_type(&probe_result, "audio");
let audio_metadata = media_helpers::extract_audio_stream_metadata(audio_stream);
let audio_codec = audio_metadata.codec;
let audio_channels = audio_metadata.channels;
let audio_channel_layout = audio_metadata.channel_layout;
let audio_sample_rate = audio_metadata.sample_rate;
let audio_bitrate = audio_metadata.bitrate;
let audio_codec_profile = media_helpers::extract_codec_profile(audio_stream);
let audio_stream_size = media_helpers::calculate_stream_size(audio_bitrate, duration_seconds);
let compression_mode = audio_codec
.as_ref()
.map(|codec| extract_compression_mode(codec));
let bit_depth = if compression_mode == Some(CompressionMode::Lossless) {
media_helpers::extract_stream_bit_depth(audio_stream)
} else {
None
};
let encoded_library = media_helpers::extract_encoded_library(audio_stream, &probe_result);
let bit_rate_mode = audio_codec
.as_ref()
.filter(|codec| mp3::is_mp3_codec(codec))
.and_then(|_| mp3::read_lame_tag_bitrate_mode(&stats_ref.file_path))
.or_else(|| {
media_helpers::extract_bitrate_mode(
audio_codec.as_ref(),
encoded_library.as_ref(),
None, )
});
let audio_language = media_helpers::extract_language(audio_stream);
let creation_time = media_helpers::extract_creation_time(audio_stream, &probe_result);
let rich_tags = extract_rich_tags(&stats_ref.file_path);
Ok(OutputAudioMetadata {
duration_seconds,
audio_codec,
audio_codec_profile,
audio_bitrate,
audio_channels,
audio_channel_layout,
audio_sample_rate,
audio_language,
container_format,
audio_stream_size,
creation_time,
title: rich_tags.title,
artist: rich_tags.artist,
album: rich_tags.album,
album_artist: rich_tags.album_artist,
track: rich_tags.track,
track_total: rich_tags.track_total,
year: rich_tags.year,
genre: rich_tags.genre,
comments: rich_tags.comments,
bit_depth,
compression_mode,
encoded_library,
bit_rate_mode,
artwork: rich_tags.artwork,
})
}
fn extract_rich_tags(file_path_ref: &str) -> RichTags {
let tagged_file = match read_from_path(file_path_ref) {
Ok(tf) => tf,
Err(e) => {
log::debug!("Failed to read tags from {file_path_ref}: {e}");
return RichTags::default();
}
};
let tag = match tagged_file.primary_tag() {
Some(t) => t,
None => {
match tagged_file.first_tag() {
Some(t) => t,
None => {
return RichTags::default();
}
}
}
};
let title = tag.title().map(|s| s.to_string());
let artist = tag.artist().map(|s| s.to_string());
let album = tag.album().map(|s| s.to_string());
let track = tag.track();
let track_total = tag.track_total();
let year = tag.date().map(|ts| u32::from(ts.year));
let genre = tag
.get_string(ItemKey::Genre)
.map(std::string::ToString::to_string);
let album_artist = tag
.get_string(ItemKey::AlbumArtist)
.map(std::string::ToString::to_string);
let comments = tag
.get_string(ItemKey::Comment)
.map(std::string::ToString::to_string);
let artwork = extract_artwork(tag);
RichTags {
title,
artist,
album,
album_artist,
track,
track_total,
year,
genre,
comments,
artwork,
}
}
fn extract_artwork(tag_ref: &lofty::tag::Tag) -> Option<ImageMetadata> {
for picture in tag_ref.pictures() {
if picture.pic_type() == PictureType::CoverFront {
let image_data = picture.data();
return analyze_image_data(image_data);
}
}
for picture in tag_ref.pictures() {
if matches!(
picture.pic_type(),
PictureType::CoverFront
| PictureType::CoverBack
| PictureType::Other
| PictureType::Artist
) {
return analyze_image_data(picture.data());
}
}
None
}
fn analyze_image_data(image_data_ref: &[u8]) -> Option<ImageMetadata> {
let stats = ParseResult {
file_path: String::new(), file_type: FileType::Image,
line_count: 0,
byte_count: image_data_ref.len(),
token_count: 0,
duration: std::time::Duration::ZERO,
is_binary: true,
..Default::default()
};
let config = RuntimeConfig::default();
match image::extract_image_metadata(image_data_ref, &stats, &config) {
Ok(metadata) => Some(metadata),
Err(e) => {
log::debug!("Failed to extract artwork metadata: {e}");
None
}
}
}
crate::no_template_mining!(
extract_audio_templates,
"Audio files don't have templates, return empty result."
);