use std::{
collections::{HashMap, hash_map},
path::Path,
};
use lofty::{
file::TaggedFileExt,
picture::PictureType,
tag::{ItemKey, TagItem, TagType},
};
use lunar_lib::{hash_file, iterator_ext::IteratorExtensions, vec_ext::VecExtensions};
use thiserror::Error;
use crate::{
library::{
Entry, Id,
album::{Album, TrackReference},
artist::Artist,
image_art::ImageArt,
metadata::{LoftyTagTakeAccessors, extract_instrumental},
track::{Track, lyric_data::LyricData, track_meta::TrackMeta},
},
media_container::ContainerError,
symphonia_helpers::{ContainerExtractResult, extract_from_file},
};
#[derive(Clone)]
pub struct ExtractResult {
pub track: Entry<Track>,
pub cover_art: Option<ImageArt>,
pub album: Option<Entry<Album>>,
pub artists: Vec<Entry<Artist>>,
}
impl ExtractResult {
#[inline]
#[cfg(feature = "database-impls")]
pub fn tx_upsert_items(
self,
cas_tx: &mut lunar_lib::database::CompareAndSwapTransaction<crate::database::Library>,
) -> Result<(), lunar_lib::database::TransactionError> {
use crate::database::SeleneEntryExt;
self.track.to_db_entry().tx_upsert(cas_tx)?;
if let Some(album) = self.album {
album.to_db_entry().tx_patch(cas_tx)?;
}
for artist in self.artists {
artist.to_db_entry().tx_patch(cas_tx)?;
}
Ok(())
}
#[must_use]
pub fn image(&self) -> Option<Entry<ImageArt>> {
self.cover_art.clone().map(|art| Entry {
id: art.id(),
entry: art,
})
}
}
#[derive(Debug, Error)]
pub enum ExtractError {
#[error("IoError: {0}")]
Io(#[from] std::io::Error),
#[error("LoftyError: {0}")]
Lofty(#[from] lofty::error::LoftyError),
#[error("Container Error: {0}")]
Container(#[from] ContainerError),
}
pub fn extract_metadata(source_file: impl AsRef<Path>) -> Result<ExtractResult, ExtractError> {
let source_file = source_file.as_ref();
let track_id = Id::<Track>::from(*hash_file(source_file)?.as_bytes());
let ContainerExtractResult { container, .. } = extract_from_file(source_file)?;
let mut metadata = lofty::read_from_path(source_file)?;
let tags = metadata.primary_tag_mut().unwrap();
let (title, mut track_artists) = tags.track_title_and_artists();
for artist in &mut track_artists {
artist.tracks.push(track_id);
}
let (track_title, instrumental) = match title {
Some(t) => {
let (extracted, instrumental) = extract_instrumental(&t);
(Some(extracted.into_owned()), Some(instrumental))
}
None => (None, None),
};
let track_artist_ids = track_artists.iter().map(|t| t.id).to_vec();
let mut all_artists: HashMap<_, _> = track_artists.into_iter().map(|a| (a.id(), a)).collect();
let date = tags.date();
let track_num = tags.track_num();
let disc_num = tags.disc_num();
let genre: Vec<String> = tags.take_strings(ItemKey::Genre).collect();
let mut album = if let Some((mut album, album_artists)) =
tags.album(track_title.as_deref(), track_num, disc_num)
{
album.date = date;
album.genre = genre.clone();
for artist in album_artists {
match all_artists.entry(artist.id()) {
hash_map::Entry::Occupied(mut e) => {
e.get_mut().albums.push_unique(album.id());
}
hash_map::Entry::Vacant(e) => {
e.insert(artist);
}
}
}
Some(album)
} else {
None
};
let lyrics = tags.lyrics();
let lyrics = if instrumental == Some(true) {
Some(LyricData::Instrumental)
} else {
lyrics
};
let cover_art = if tags.picture_count() != 0 {
let picture_idx = tags
.pictures()
.iter()
.position(|p| p.pic_type() == PictureType::CoverFront)
.unwrap_or(0);
let picture = tags.remove_picture(picture_idx);
ImageArt::try_from_picture(picture)
} else {
None
};
let other = tags
.items()
.cloned()
.map(TagItem::consume)
.filter_map(|(k, v)| {
let v = v.text()?.to_owned();
let k = k.map_key(TagType::VorbisComments)?.to_owned();
Some((k, v))
});
let metadata = TrackMeta {
album: album.as_ref().map(Entry::id),
artists: track_artist_ids,
date,
genre,
lyric_data: lyrics,
other: other.collect(),
title: track_title,
art: cover_art.as_ref().map(ImageArt::id),
};
let track = Track::new(container, metadata)?;
if let Some(album) = &mut album {
album.tracks.push(TrackReference {
id: track_id,
track_num,
disc_num,
});
}
#[cfg(debug_assertions)]
{
assert!(track.metadata.album == album.as_ref().map(Entry::id));
if let Some(album) = &album {
assert!(album.tracks.first().unwrap().id == track_id);
}
for artist in &track.metadata.artists {
assert!(all_artists.get(artist).unwrap().tracks[0] == track_id);
}
for artist in all_artists.values() {
if let Some(next) = artist.albums.first() {
assert!(album.as_ref().unwrap().id() == *next);
}
if let Some(next) = artist.tracks.first() {
assert!(track.metadata.artists.contains(&artist.id()));
assert!(track_id == *next);
}
}
}
Ok(ExtractResult {
track: Entry {
entry: track,
id: track_id,
},
album,
artists: all_artists.into_values().collect(),
cover_art,
})
}