use log::info;
use crate::{cue::BlissCue, song::AnalysisOptions, BlissError, BlissResult, Song};
use std::{
num::NonZeroUsize,
path::{Path, PathBuf},
sync::mpsc,
thread,
time::Duration,
};
#[derive(Default, Debug)]
pub struct PreAnalyzedSong {
pub path: PathBuf,
pub artist: Option<String>,
pub album_artist: Option<String>,
pub title: Option<String>,
pub album: Option<String>,
pub track_number: Option<i32>,
pub disc_number: Option<i32>,
pub genre: Option<String>,
pub duration: Duration,
pub sample_array: Vec<f32>,
}
impl TryFrom<PreAnalyzedSong> for Song {
type Error = BlissError;
fn try_from(raw_song: PreAnalyzedSong) -> BlissResult<Song> {
raw_song.to_song_with_options(AnalysisOptions::default())
}
}
impl PreAnalyzedSong {
fn to_song_with_options(&self, analysis_options: AnalysisOptions) -> BlissResult<Song> {
Ok(Song {
path: self.path.clone(),
artist: self.artist.clone(),
album_artist: self.album_artist.clone(),
title: self.title.clone(),
album: self.album.clone(),
track_number: self.track_number,
disc_number: self.disc_number,
genre: self.genre.clone(),
duration: self.duration,
analysis: Song::analyze_with_options(&self.sample_array, &analysis_options)?,
features_version: analysis_options.features_version,
cue_info: None,
})
}
}
pub trait Decoder {
fn decode(path: &Path) -> BlissResult<PreAnalyzedSong>;
fn song_from_path<P: AsRef<Path>>(path: P) -> BlissResult<Song> {
Self::decode(path.as_ref())?.try_into()
}
fn song_from_path_with_options<P: AsRef<Path>>(
path: P,
analysis_options: AnalysisOptions,
) -> BlissResult<Song> {
Self::decode(path.as_ref())?.to_song_with_options(analysis_options)
}
#[cfg_attr(
feature = "ffmpeg",
doc = r##"
# Example
```no_run
use bliss_audio::{BlissResult};
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
fn main() -> BlissResult<()> {
let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
for (path, result) in Decoder::analyze_paths(&paths) {
match result {
Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
}
}
Ok(())
}
```"##
)]
fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
paths: F,
) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
Self::analyze_paths_with_options(paths, AnalysisOptions::default())
}
#[cfg_attr(
feature = "ffmpeg",
doc = r##"
# Example
```no_run
use bliss_audio::BlissResult;
use bliss_audio::decoder::Decoder as DecoderTrait;
use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
fn main() -> BlissResult<()> {
let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
for (path, result) in Decoder::analyze_paths(&paths) {
match result {
Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
}
}
Ok(())
}
```"##
)]
fn analyze_paths_with_options<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
paths: F,
analysis_options: AnalysisOptions,
) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
let desired_number_cores = analysis_options.number_cores;
if cores > desired_number_cores {
cores = desired_number_cores;
}
let paths: Vec<PathBuf> = paths.into_iter().map(|p| p.into()).collect();
#[allow(clippy::type_complexity)]
let (tx, rx): (
mpsc::Sender<(PathBuf, BlissResult<Song>)>,
mpsc::Receiver<(PathBuf, BlissResult<Song>)>,
) = mpsc::channel();
if paths.is_empty() {
return rx.into_iter();
}
let mut handles = Vec::new();
let mut chunk_length = paths.len() / cores;
if chunk_length == 0 {
chunk_length = paths.len();
}
for chunk in paths.chunks(chunk_length) {
let tx_thread = tx.clone();
let owned_chunk = chunk.to_owned();
let child = thread::spawn(move || {
for path in owned_chunk {
info!("Analyzing file '{path:?}'");
if let Some(extension) = Path::new(&path).extension() {
let extension = extension.to_string_lossy().to_lowercase();
if extension == "cue" {
match BlissCue::<Self>::songs_from_path(&path) {
Ok(songs) => {
for song in songs {
tx_thread.send((path.to_owned(), song)).unwrap();
}
}
Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(),
};
continue;
}
}
let song = Self::song_from_path_with_options(&path, analysis_options);
tx_thread.send((path.to_owned(), song)).unwrap();
}
});
handles.push(child);
}
rx.into_iter()
}
}
#[cfg(feature = "symphonia")]
pub mod symphonia;
#[cfg(feature = "ffmpeg")]
pub mod ffmpeg;