use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
sync::Arc,
};
use barber::{ProgressBar, ProgressRenderer};
use lunar_lib::formatter::{FormatTable, format_str};
use rayon::{
ThreadPoolBuilder,
iter::{IntoParallelRefIterator, ParallelIterator},
};
use crate::{
config::common::common_config,
database::{DatabaseEntry, DatabaseError, db_transaction},
errors::ExtractError,
ffmpeg::{ffprobe_format_tags, loudnorm::LoudnormAnalysis},
library::{
artist::{Artist, ArtistGroup, add_from_artists, extract_from_featuring},
cover_art::{CoverArt, has_video_stream},
import::ExtractResult,
track::{
Track, TrackId,
track_meta::{AlbumReference, TrackMeta},
},
},
media_container::MediaContainer,
utils::{hash_file, pair_extension},
};
#[must_use]
pub fn scan_for_extract<'a>(
known_tracks: &'a [TrackId],
sources: &'a HashMap<TrackId, PathBuf>,
) -> Vec<&'a Path> {
sources
.iter()
.filter_map(|(id, path)| {
if known_tracks.contains(id) {
None
} else {
Some(path.as_path())
}
})
.collect()
}
pub fn extract(
files: &[&Path],
analyze_loudnorm: bool,
progress_renderer: Arc<dyn ProgressRenderer>,
) -> Result<(), ExtractError> {
if files.is_empty() {
return Ok(());
}
let progress_bar = ProgressBar::new(0, files.len(), progress_renderer);
progress_bar.set_label("Extracting metadata from files...");
let max_threads = (rayon::current_num_threads() as f32 * 0.33).ceil() as usize;
let thread_pool = ThreadPoolBuilder::new()
.num_threads(max_threads)
.build()
.unwrap();
thread_pool.install(|| {
files
.par_iter()
.try_for_each(|source| -> Result<(), ExtractError> {
let ExtractResult {
track,
album,
artists,
} = extract_metadata(source, analyze_loudnorm)?;
db_transaction(|cas_tx| -> Result<(), DatabaseError> {
track.tx_insert(cas_tx)?;
if let Some(album) = &album {
album.tx_patch(cas_tx)?;
}
for artist in &artists {
artist.tx_patch(cas_tx)?;
}
Ok(())
})?;
progress_bar.set_label(&format!(
"Extracted metadata from '{path}'",
path = source.display()
));
progress_bar.increment();
Ok(())
})
})?;
progress_bar.flush();
Ok(())
}
pub fn extract_metadata(
source_file: impl AsRef<Path>,
loudnorm: bool,
) -> Result<ExtractResult, ExtractError> {
let source_file = source_file.as_ref();
let src_container = MediaContainer::from_file(source_file.to_path_buf())?;
let (container, codec) = src_container
.transcode_to()
.ok_or(ExtractError::InvalidContainer)?;
let mut all_artists: HashSet<Artist> = HashSet::new();
let tags = ffprobe_format_tags(source_file)?;
let track_num = tags.extract_track_num().0;
let disc_num = tags.extract_disc_num().0;
let date = tags.extract_date();
let mut track_artists = tags.extract_track_artists();
let title = if let Some(title) = &tags.title {
let (title, other_artists) = extract_from_featuring(title);
track_artists.extend(other_artists);
Some(title.to_string())
} else {
None
};
all_artists.extend(track_artists.clone());
let lyric_data = tags.extract_lyric_data();
let album = tags.extract_album(&track_artists).map(|(album, artists)| {
all_artists.extend(artists.clone());
album
});
let metadata = TrackMeta {
album: album
.as_ref()
.map(|a| AlbumReference::new(a.id(), track_num, disc_num)),
artists: ArtistGroup::from_artists(&track_artists),
date,
genre: tags.genre,
lyric_data,
other: tags.other,
title,
};
let relative_path = {
let mut format_table = FormatTable::new();
format_table.extend_from_taggable(&metadata);
add_from_artists(&mut format_table, &track_artists, "track");
if let Some(album) = &album {
format_table.extend_from_taggable(album);
}
let mut path = format_str(
&common_config().track_name_config.format_string,
&format_table,
)?;
path.push('.');
path.push_str(pair_extension(&container, &codec).unwrap());
PathBuf::from(path)
};
let mut track = Track::new(
hash_file(source_file)?,
src_container,
metadata,
relative_path,
);
if loudnorm {
track.loudnorm_analysis = Some(LoudnormAnalysis::from_file(source_file)?);
}
if let Some(source) = has_video_stream(source_file).then_some(source_file.to_path_buf()) {
let cover_art = CoverArt::from_file(&source)?;
track.cover_art = Some(cover_art);
}
let result = ExtractResult {
track,
album,
artists: all_artists.into_iter().collect(),
};
Ok(result)
}