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::{DatabaseError, writer::db_sync_transaction},
errors::ExtractError,
ffmpeg::{ffprobe_format_tags, loudnorm::LoudnormAnalysis},
library::{
album::{Album, TrackReference, UNKNOWN_ALBUM},
artist::{Artist, ArtistGroup, add_from_artists, extract_from_featuring},
cover_art::{CoverArt, has_video_stream},
import::ExtractResult,
metadata::extract_date_str,
track::{Track, TrackId, track_meta::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_sync_transaction(
move |cas_tx| -> Result<(), DatabaseError> {
cas_tx.tx_patch(track.clone())?;
if let Some(album) = &album {
cas_tx.tx_patch(album.clone())?;
}
for artist in &artists {
cas_tx.tx_patch(artist.clone())?;
}
Ok(())
},
true,
)?;
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 raw_metadata = ffprobe_format_tags(source_file)?;
let mut all_artists = HashSet::new();
let mut track_artists = raw_metadata.extract_track_artists();
let (track_num, track_total) = raw_metadata.extract_track_num();
let (disc_num, disc_total) = raw_metadata.extract_disc_num();
let date = raw_metadata.date.as_deref().and_then(extract_date_str);
let genre = raw_metadata.genre.clone();
let lyric_data = raw_metadata.extract_lyric_data();
let mut album = {
let mut album_artists = raw_metadata.extract_album_artists();
let album_name_differs = raw_metadata
.album
.as_deref()
.zip(raw_metadata.title.as_deref())
.is_none_or(|(a, b)| a != b);
let album_artist_differs = album_artists != track_artists;
let multiple_tracks_or_discs = {
track_num.is_some_and(|v| v > 1)
|| track_total.is_some_and(|v| v > 1)
|| disc_num.is_some_and(|v| v > 1)
|| disc_total.is_some_and(|v| v > 1)
};
if album_name_differs || multiple_tracks_or_discs || album_artist_differs {
let mut album = Album::new(
raw_metadata.album.unwrap_or(UNKNOWN_ALBUM.to_owned()),
ArtistGroup::from_artists(&album_artists),
Vec::new(),
);
for artist in &mut album_artists {
artist.albums.push(album.id());
}
all_artists.extend(album_artists);
album.date = date;
album.track_total = track_total;
album.disc_total = disc_total;
album.genre = genre.clone();
Some(album)
} else {
None
}
};
let title = if let Some(title) = raw_metadata.title {
let (title, other_artists) = extract_from_featuring(&title);
track_artists.extend(other_artists);
Some(title.to_string())
} else {
None
};
let cover_art = has_video_stream(source_file)
.then_some(CoverArt::from_file(source_file))
.transpose()?;
let metadata = TrackMeta {
album: album.as_ref().map(Album::id),
artists: ArtistGroup::from_artists(&track_artists),
date,
genre,
lyric_data,
other: raw_metadata.other,
title,
cover_art,
};
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 let Some(album) = &mut album {
album.tracks.push(TrackReference {
id: track.id(),
track_num,
disc_num,
});
}
if loudnorm {
track.loudnorm_analysis = Some(LoudnormAnalysis::from_file(source_file)?);
}
all_artists.extend(track_artists.clone());
let mut all_artists: Vec<Artist> = all_artists.into_iter().collect();
for artist in &mut all_artists {
if album
.as_ref()
.is_none_or(|a| !a.artist_group.artist_ids().contains(&artist.id()))
{
artist.tracks.push(track.id());
}
}
let result = ExtractResult {
track,
album,
artists: all_artists,
};
Ok(result)
}