use anyhow::{anyhow,bail,ensure};
use log::{info,error,warn,trace,debug};
use lofty::{
Accessor,
TaggedFile,
TaggedFileExt,
AudioFile,
Picture,
};
use std::path::{Path,PathBuf};
use std::collections::HashMap;
use crate::collection::{
Art,
Artist,
Album,
Song,
ArtistKey,
AlbumKey,
SongKey,
};
use crate::macros::{
lock,
skip_warn,
unwrap_or_mass,
};
use crate::constants::{
SKIP
};
use crossbeam_channel::Sender;
use super::CcdToKernel;
use readable::{Runtime,Int};
use std::borrow::Cow;
use std::sync::{Arc,Mutex};
#[derive(Debug)]
struct TagMetadata<'a> {
artist: Cow<'a, str>,
album: Cow<'a, str>,
title: Cow<'a, str>,
track: Option<u32>,
disc: Option<u32>,
track_total: Option<u32>,
disc_total: Option<u32>,
picture: Option<Vec<u8>>,
runtime: f64,
release: Option<&'a str>,
track_artists: Option<String>,
compilation: bool,
}
impl super::Ccd {
#[inline(always)]
pub(super) fn audio_paths_to_incomplete_vecs(
to_kernel: &Sender<CcdToKernel>,
vec_paths: Vec<PathBuf>
) -> (Vec<Artist>, Vec<Album>, Vec<Song>) {
let mut memory: Mutex<HashMap<String, (usize, HashMap<String, usize>)>> = Mutex::new(HashMap::new());
let mut vec_artist: Mutex<Vec<Artist>> = Mutex::new(vec![]);
let mut vec_album: Mutex<Vec<Album>> = Mutex::new(vec![]);
let mut vec_song: Mutex<Vec<Song>> = Mutex::new(vec![]);
let mut count_artist: Mutex<usize> = Mutex::new(0);
let mut count_album: Mutex<usize> = Mutex::new(0);
let mut count_song: Mutex<usize> = Mutex::new(0);
let threads = super::threads_for_paths(vec_paths.len());
std::thread::scope(|scope| {
for paths in vec_paths.chunks(threads) {
scope.spawn(|| {
for path in paths.into_iter() {
let path = path.clone();
let mut tagged_file = match Self::path_to_tagged_file(&path) {
Ok(t) => t,
Err(e) => { warn!("CCD | TaggedFile fail: {}{}", path.display(), SKIP); continue; },
};
let mut tag = match Self::tagged_file_to_tag(&mut tagged_file) {
Ok(t) => t,
Err(e) => { warn!("CCD | Tag fail: {}{}", path.display(), SKIP); continue; },
};
let metadata = match Self::extract_tag_metadata(tagged_file, &mut tag) {
Ok(t) => t,
Err(e) => { warn!("CCD | Metadata fail: {}{}", path.display(), SKIP); continue; },
};
let TagMetadata {
artist,
album,
title,
track,
disc,
track_total,
disc_total,
picture,
runtime,
release,
track_artists,
compilation,
} = metadata;
if let Some((artist_idx, album_map)) = lock!(memory).get_mut(&*artist) {
if let Some(album_idx) = album_map.get(&*album) {
let song = Song {
title: title.to_string(),
album: AlbumKey::from(*album_idx),
runtime_human: Runtime::from(runtime),
track,
track_artists,
disc,
runtime,
path,
};
lock!(vec_album)[*album_idx].songs.push(SongKey::from(*lock!(count_song)));
lock!(vec_song).push(song);
*lock!(count_song) += 1;
continue
}
let song = Song {
title: title.to_string(),
album: AlbumKey::from(*lock!(count_album)),
runtime_human: Runtime::from(runtime),
track,
track_artists,
disc,
runtime,
path,
};
let art_bytes = match picture {
Some(p) => Some(p),
None => None,
};
let release = match release {
Some(date) => Self::parse_str_date(date),
None => (None, None, None),
};
let album_struct = Album {
title: album.to_string(),
artist: ArtistKey::from(*lock!(count_artist)),
release_human: Self::date_to_string(release),
songs: vec![SongKey::from(*lock!(count_song))],
release,
art_bytes,
compilation,
song_count_human: Int::new(),
runtime_human: Runtime::zero(),
runtime: 0.0,
song_count: 0,
art: Art::Unknown,
};
lock!(vec_artist)[*artist_idx].albums.push(AlbumKey::from(*lock!(count_album)));
lock!(vec_album).push(album_struct);
lock!(vec_song).push(song);
album_map.insert(album.to_string(), *lock!(count_album));
*lock!(count_album) += 1;
*lock!(count_song) += 1;
continue
}
let song = Song {
title: title.to_string(),
album: AlbumKey::from(*lock!(count_album)),
runtime_human: Runtime::from(runtime),
track,
track_artists,
disc,
runtime,
path,
};
let art_bytes = match picture {
Some(p) => Some(p),
None => None,
};
let release = match release {
Some(date) => Self::parse_str_date(date),
None => (None, None, None),
};
let album_struct = Album {
title: album.to_string(),
artist: ArtistKey::from(*lock!(count_artist)),
release_human: Self::date_to_string(release),
songs: vec![SongKey::from(*lock!(count_song))],
release,
art_bytes,
compilation,
song_count_human: Int::new(),
runtime_human: Runtime::zero(),
runtime: 0.0,
song_count: 0,
art: Art::Unknown,
};
let artist_struct = Artist {
name: artist.to_string(),
albums: vec![AlbumKey::from(*lock!(count_album))],
};
lock!(vec_artist).push(artist_struct);
lock!(vec_album).push(album_struct);
lock!(vec_song).push(song);
lock!(memory).insert(
artist.to_string(),
(*lock!(count_artist), HashMap::from([(album.to_string(), *lock!(count_album))]))
);
*lock!(count_artist) += 1;
*lock!(count_album) += 1;
*lock!(count_song) += 1;
} }); } });
let (vec_artist, vec_album, vec_song) = (vec_artist.into_inner(), vec_album.into_inner(), vec_song.into_inner());
let (vec_artist, vec_album, vec_song) = (unwrap_or_mass!(vec_artist), unwrap_or_mass!(vec_album), unwrap_or_mass!(vec_song));
(vec_artist, vec_album, vec_song)
}
#[inline(always)]
pub(super) fn fix_album_metadata_from_songs(vec_album: &mut Vec<Album>, vec_song: &Vec<Song>) {
for album in vec_album {
let song_count = album.songs.len();
album.song_count = song_count;
album.song_count_human = Int::from(song_count);
let mut runtime = 0.0;
album.songs.iter().for_each(|key| runtime += vec_song[key.inner()].runtime);
album.runtime_human = Runtime::from(runtime);
album.runtime = runtime;
}
}
#[inline(always)]
fn path_to_tagged_file(path: &Path) -> Result<lofty::TaggedFile, anyhow::Error> {
use std::fs::File;
use std::io::BufReader;
let file = File::open(path)?;
let reader = BufReader::new(file);
let options = lofty::ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed);
let probe = lofty::Probe::new(reader).options(options);
Ok(probe.guess_file_type()?.read()?)
}
#[inline(always)]
fn tagged_file_to_tag(tagged_file: &mut lofty::TaggedFile) -> Result<lofty::Tag, anyhow::Error> {
if let Some(t) = tagged_file.remove(lofty::TagType::VorbisComments) {
Ok(t)
} else if let Some(t) = tagged_file.remove(lofty::TagType::ID3v2) {
Ok(t)
} else if let Some(t) = tagged_file.remove(lofty::TagType::ID3v1) {
Ok(t)
} else {
Err(anyhow!("No tag"))
}
}
#[inline(always)]
fn tagged_file_runtime(tagged_file: lofty::TaggedFile) -> f64 {
tagged_file.properties().duration().as_secs_f64()
}
#[inline]
fn item_value_to_str<'a>(item: &'a lofty::ItemValue) -> Option<&'a str> {
match item {
lofty::ItemValue::Text(s) => Some(s),
lofty::ItemValue::Locator(s) => Some(s),
lofty::ItemValue::Binary(b) => {
if let Ok(s) = std::str::from_utf8(b) {
Some(s)
} else {
None
}
},
}
}
#[inline(always)]
fn tag_release<'a>(tag: &'a lofty::Tag) -> Option<&'a str> {
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::OriginalReleaseDate) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
return Some(s)
}
}
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::RecordingDate) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
return Some(s)
}
}
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::Year) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
return Some(s)
}
}
None
}
#[inline(always)]
fn tag_track_artists(tag: &lofty::Tag) -> Option<String> {
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::Performer) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
return Some(s.to_string())
}
}
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::TrackArtist) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
return Some(s.to_string())
}
}
None
}
#[inline(always)]
fn tag_compilation<'a>(artist: &str, tag: &'a lofty::Tag) -> bool {
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::FlagCompilation) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
if s == "1" {
return true
}
}
}
if let Some(t) = tag.get_item_ref(&lofty::ItemKey::AlbumArtist) {
if let Some(s) = Self::item_value_to_str(&t.value()) {
if s == "Various Artists" && s != artist {
return true
}
}
}
false
}
#[inline(always)]
fn extract_tag_metadata<'a>(tagged_file: lofty::TaggedFile, tag: &'a mut lofty::Tag) -> Result<TagMetadata<'a>, anyhow::Error> {
let picture = {
if tag.pictures().len() == 0 {
None
} else {
Some(tag.remove_picture(0).data().to_vec())
}
};
let artist = match tag.artist() { Some(t) => t, None => bail!("No artist") };
let album = match tag.album() { Some(t) => t, None => bail!("No album") };
let title = match tag.title() { Some(t) => t, None => bail!("No title") };
let track = tag.track();
let disc = tag.disk();
let track_total = tag.track_total();
let disc_total = tag.disk_total();
let runtime = Self::tagged_file_runtime(tagged_file);
let release = Self::tag_release(tag);
let track_artists = Self::tag_track_artists(tag);
let compilation = Self::tag_compilation(&artist, tag);
Ok(TagMetadata {
artist,
album,
title,
track,
disc,
track_total,
disc_total,
picture,
runtime,
release,
track_artists,
compilation,
})
}
}
#[cfg(test)]
mod tests {
use crate::ccd::Ccd;
use std::path::PathBuf;
use lofty::TaggedFile;
#[test]
#[ignore]
fn vecs() {
let paths = vec![
PathBuf::from("assets/audio/rain.mp3"),
PathBuf::from("assets/audio/rain.flac"),
PathBuf::from("assets/audio/rain.ogg"),
];
let (to_kernel, _) = crossbeam_channel::unbounded::<super::CcdToKernel>();
let (vec_artist, mut vec_album, vec_song) = Ccd::audio_paths_to_incomplete_vecs(&to_kernel, paths);
println!("{:#?}", vec_artist);
println!("{:#?}", vec_album);
println!("{:#?}", vec_song);
assert!(vec_artist.len() == 1);
assert!(vec_album.len() == 1);
assert!(vec_song.len() == 3);
assert!(vec_artist[0].name == "hinto");
assert!(vec_artist[0].albums.len() == 1);
assert!(vec_album[0].title == "Festival");
assert!(vec_album[0].artist.inner() == 0);
assert!(vec_album[0].release_human == "2023-03-08");
assert!(vec_album[0].songs.len() == 3);
assert!(vec_album[0].release == (Some(2023), Some(3), Some(8)));
assert!(vec_album[0].compilation == true);
Ccd::fix_album_metadata_from_songs(&mut vec_album, &vec_song);
println!("{:#?}", vec_artist);
println!("{:#?}", vec_album);
println!("{:#?}", vec_song);
assert!(vec_album[0].runtime_human == readable::Runtime::from(5.83));
assert!(vec_album[0].song_count_human.as_str() == "3");
assert!(vec_album[0].runtime == 5.83);
assert!(vec_album[0].song_count == 3);
}
fn mp3() -> TaggedFile {
let mp3 = Ccd::path_to_tagged_file(PathBuf::from("assets/audio/rain.mp3").as_path()).unwrap();
mp3
}
#[test]
fn runtime() {
let mp3 = mp3();
let runtime = Ccd::tagged_file_runtime(&mp3);
eprintln!("{}", runtime);
assert!(runtime == 1.968);
}
#[test]
fn release() {
let mp3 = mp3();
let tag = Ccd::tagged_file_to_tag(&mp3).unwrap();
let release = Ccd::tag_release(&tag).unwrap();
eprintln!("{}", release);
assert!(release == "2023-03-08");
}
#[test]
fn track_artists() {
let mp3 = mp3();
let tag = Ccd::tagged_file_to_tag(&mp3).unwrap();
let track_artist = Ccd::tag_track_artists(tag).unwrap();
eprintln!("{}", track_artist);
assert!(track_artist == "hinto");
}
#[test]
fn compilation() {
let mp3 = mp3();
let tag = Ccd::tagged_file_to_tag(&mp3).unwrap();
let comp = Ccd::tag_compilation("hinto", tag);
eprintln!("{}", comp);
assert!(comp);
}
#[test]
fn extract() {
let mp3 = mp3();
let tag = Ccd::tagged_file_to_tag(&mp3).unwrap();
let meta = Ccd::extract_tag_metadata(&mp3, &tag).unwrap();
eprintln!("{:#?}", meta);
assert!(meta.artist == "hinto");
assert!(meta.album == "Festival");
assert!(meta.title == "rain_mp3");
assert!(meta.track == Some(1));
assert!(meta.disc == None);
assert!(meta.track_total == None);
assert!(meta.disc_total == None);
assert!(meta.picture == None);
assert!(meta.runtime == 1.968);
assert!(meta.release == Some("2023-03-08"));
assert!(meta.track_artists == Some("hinto".to_string()));
assert!(meta.compilation == true);
}
}