use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
};
use barber::{ProgressBar, ProgressRenderer};
use lunar_lib::{
database::{DatabaseEntry, DatabaseError, writer::DatabaseWriter},
error,
};
use rayon::{
ThreadPoolBuilder,
iter::{IntoParallelIterator, ParallelIterator},
};
use crate::{
VALID_LIBRARY_CODECS, VALID_LIBRARY_FORMATS,
config::common::common_config,
database::{LibraryDb, tx_extensions::CasTxExtensions},
errors::{CodecError, ContainerError, ImportError, LibraryError},
ffmpeg::{FfmpegPresets, ffmpeg, output_to_string},
library::{album::Album, artist::Artist, track::Track},
media_container::MediaContainer,
utils::pair_extension,
};
#[derive(Debug, Clone)]
pub struct ExtractResult {
pub track: Track,
pub album: Option<Album>,
pub artists: Vec<Artist>,
}
#[derive(Debug, Clone, Copy)]
pub struct TrackImportOptions {
pub dry: bool,
pub apply_loudnorm: bool,
}
pub fn find_needs_import(loudnorm: bool) -> Result<Vec<Track>, DatabaseError> {
let mut tracks = Track::db_get_all()?;
let loudnorm_with = loudnorm.then_some(common_config().loudnorm);
tracks.retain(|track| {
let missing_lib = track.lib_container().is_none_or(|c| !c.path().is_file());
let needs_loudnorm = loudnorm_with.is_some_and(|cfg| track.loudnorm() != Some(&cfg));
missing_lib || needs_loudnorm
});
Ok(tracks)
}
pub fn reimport(
tracks: Vec<Track>,
progress_renderer: Arc<dyn ProgressRenderer>,
options: TrackImportOptions,
) -> Result<(), ImportError> {
if tracks.is_empty() {
return Ok(());
}
let library_dir = common_config()
.library_dir()
.ok_or(LibraryError::NoLibrary)?
.to_path_buf();
for track in &tracks {
track.ensure_parent_dirs(&library_dir)?;
}
let progress_bar = ProgressBar::new(0, tracks.len(), progress_renderer);
progress_bar.set_label("Importing 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();
drop(common_config());
let writer = DatabaseWriter::<LibraryDb>::spawn();
thread_pool.install(|| {
tracks
.into_par_iter()
.try_for_each(|track| -> Result<(), DatabaseError> {
if writer.is_closed() {
return Ok(());
}
let import_to = library_dir.join(&track.relative_library_path);
let track_path = track.relative_library_path.display().to_string();
match import(track, import_to, options) {
Ok(track) => {
if writer.is_closed() {
return Ok(());
}
writer.transaction(move |cas_tx| cas_tx.tx_patch(track.clone()));
progress_bar.set_label(&format!("Imported '{track_path}'",));
}
Err(err) => {
let args = format_args!("Failed to import {track_path}: {err}");
error!("{args}");
progress_bar.set_label(&format!("{args}"));
}
}
progress_bar.increment();
Ok(())
})
})?;
writer.finish()?;
progress_bar.flush();
Ok(())
}
pub fn import(
mut track: Track,
to: impl AsRef<Path>,
options: TrackImportOptions,
) -> Result<Track, ImportError> {
let audio_source = track.src_container().path();
let src_container = *track.src_container().container();
let src_codec = *track.src_container().codec();
let common_config = common_config();
let transcode_target = common_config.transcode_to(src_container, src_codec);
let (target_container, target_codec) = if let Some(transcode_target) = transcode_target {
transcode_target.container_codec()
} else {
(src_container, src_codec)
};
if !VALID_LIBRARY_FORMATS.contains(&target_container) {
return Err(ImportError::Container(
ContainerError::UnsupportedContainer(target_container),
));
}
if !VALID_LIBRARY_CODECS.contains(&target_codec) {
return Err(ImportError::Codec(CodecError::UnsupportedCodec(
target_codec,
)));
}
let mut command = ffmpeg();
command.input_file(audio_source);
if let Some(cover) = track.metadata.cover_art() {
if cover.source() == audio_source {
command.cover_art_from(0);
} else {
command.input_file(cover.source());
command.cover_art_from(1);
}
}
if let Some(transcode_target) = transcode_target {
transcode_target.add_ffmpeg_args(&mut command);
} else {
command.set_container(target_container.format_name());
if options.apply_loudnorm {
command.set_codec(target_codec.ffmpeg_encoder()?);
} else {
command.copy_codec();
}
}
if options.apply_loudnorm
&& let Some(loudnorm_analysis) = track.loudnorm_analysis
{
command.apply_loudnorm_filter(loudnorm_analysis);
}
command.map_audio_from(0);
command.drop_subtitles();
command.drop_metadata();
let metadata = track.metadata_key_values(&target_container)?;
command.add_metadata_group(metadata);
let mut output_file = to.as_ref().as_os_str().to_owned();
output_file.push(".");
output_file.push(pair_extension(target_container, target_codec).unwrap());
let output_file = PathBuf::from(output_file);
command.output_file(&output_file);
output_to_string(command, false)?;
let opened_file = fs::File::open(&output_file)?;
track.lib_container = Some(MediaContainer::from_file(opened_file, output_file)?);
Ok(track)
}