use std::{collections::HashSet, io::Error};
use lofty::{
config::WriteOptions,
file::{
AudioFile,
FileType::{Flac, Mpeg},
},
picture::{MimeType::Jpeg, Picture, PictureType::CoverFront},
prelude::{
Accessor,
ItemKey::{
self, AlbumArtist, CommercialInformationUrl, Composer, CopyrightMessage, Isrc, Label,
MusicianCredits, OriginalMediaType, RecordingDate, ReleaseDate,
},
TagExt, TaggedFileExt,
},
read_from_path,
tag::{
ItemValue::{self, Text},
Tag, TagItem,
TagType::{Id3v2, VorbisComments},
},
};
use crate::{
errors::QobuzApiError::{self, IoError, LoftyError},
metadata::MetadataConfig,
models::{Album, Artist, Track},
utils::{download_image, timestamp_to_date_and_year},
};
pub async fn embed_metadata_in_file(
filepath: &str,
track: &Track,
album: &Album,
artist: &Artist,
config: &MetadataConfig,
) -> Result<(), QobuzApiError> {
let mut tagged_file = read_from_path(filepath).map_err(LoftyError)?;
let file_type = tagged_file.file_type();
let tag = match tagged_file.primary_tag_mut() {
Some(primary_tag) => primary_tag,
None => {
if let Some(tag) = tagged_file.first_tag_mut() {
tag
} else {
let tag_type = match file_type {
Flac => VorbisComments,
Mpeg => Id3v2,
_ => Id3v2, };
let new_tag = Tag::new(tag_type);
tagged_file.insert_tag(new_tag);
tagged_file.primary_tag_mut().ok_or_else(|| {
IoError(Error::other(
"Could not create or access tag for metadata embedding",
))
})?
}
}
};
tag.clear();
if config.track_title
&& let Some(ref title) = track.title
{
let mut full_title = title.clone();
if let Some(ref version) = track.version
&& !version.is_empty()
{
full_title = format!("{} ({})", full_title, version);
}
tag.set_title(full_title);
}
if config.album
&& let Some(ref album_title) = album.title
{
let album_name = if let Some(ref version) = album.version {
if !version.is_empty() {
format!("{} ({})", album_title, version)
} else {
album_title.clone()
}
} else {
album_title.clone()
};
tag.set_album(album_name);
}
let album_artist_for_flac = {
let mut result = String::new();
if let Some(ref album_artists) = album.artists {
let conductor_artist = album_artists.iter().find(|artist| {
artist.roles.as_ref().is_some_and(|roles| {
roles.contains(&"main-artist".to_string())
&& artist.name.as_ref().is_some_and(|name| {
track.performers.as_ref().is_some_and(|performers_str| {
performers_str.contains(&format!("{}, Conductor", name))
})
})
})
});
if let Some(artist) = conductor_artist {
result = artist.name.clone().unwrap_or_default();
} else if let Some(ref album_artist) = album.artist
&& let Some(ref name) = album_artist.name
{
result = name.clone();
}
} else if let Some(ref album_artist) = album.artist
&& let Some(ref name) = album_artist.name
{
result = name.clone();
}
result
};
let album_artist_for_mp3 = {
let mut main_artists_from_album = Vec::new();
if let Some(ref album_artists) = album.artists {
for artist_in_list in album_artists {
if let Some(ref roles) = artist_in_list.roles
&& roles.contains(&"main-artist".to_string())
&& let Some(ref name) = artist_in_list.name
{
main_artists_from_album.push(name.clone());
}
}
}
if main_artists_from_album.is_empty()
&& let Some(ref album_artist) = album.artist
&& let Some(ref name) = album_artist.name
{
main_artists_from_album.push(name.clone());
}
main_artists_from_album.join("/")
};
let album_artist_name = match file_type {
Flac => album_artist_for_flac,
Mpeg => album_artist_for_mp3,
_ => album_artist_for_mp3, };
if config.album_artist && !album_artist_name.is_empty() {
let tag_item = TagItem::new(AlbumArtist, Text(album_artist_name));
tag.push(tag_item);
}
let mut artist_names = Vec::new();
let mut artist_set = HashSet::new();
if let Some(ref track_performers) = track.performers {
let performer_artists = extract_artist_names_from_performers(track_performers, &artist_set);
for performer_artist in performer_artists {
if !artist_set.contains(&performer_artist) {
artist_names.push(performer_artist.clone());
artist_set.insert(performer_artist.clone());
}
}
}
if config.producer
&& file_type == Flac
&& let Some(ref performers_str) = track.performers
{
let producers = extract_producers_from_performers(performers_str);
for producer in producers {
let tag_item = TagItem::new(
ItemKey::from_key(VorbisComments, "PRODUCER"),
Text(producer.clone()),
);
tag.push(tag_item);
}
}
if let Some(ref artist_name) = artist.name
&& !artist_set.contains(artist_name)
{
artist_names.push(artist_name.clone());
artist_set.insert(artist_name.clone());
}
if let Some(ref album_artists) = album.artists {
for album_artist in album_artists {
if let Some(ref name) = album_artist.name
&& !name.is_empty()
&& !artist_set.contains(name)
{
artist_names.push(name.clone());
artist_set.insert(name.clone());
}
}
}
if config.artist && !artist_names.is_empty() {
let combined_artists = match file_type {
Flac => artist_names.join(", "),
_ => artist_names.join("/"),
};
tag.set_artist(combined_artists);
}
let mut composers = Vec::new();
let mut composer_normalized_set = HashSet::new();
if file_type == Flac {
let mut potential_composers_from_performers = Vec::new();
if let Some(ref performers_str) = track.performers {
potential_composers_from_performers = extract_composers_from_performers(performers_str);
}
if let Some(composer_from_performers) = potential_composers_from_performers.last() {
if composer_from_performers != "Various Composers" {
composers.push(composer_from_performers.clone());
composer_normalized_set.insert(normalize_composer_name(composer_from_performers));
}
} else if let Some(ref track_composer) = track.composer
&& let Some(ref composer_name) = track_composer.name
&& composer_name != "Various Composers"
{
composers.push(composer_name.clone());
composer_normalized_set.insert(normalize_composer_name(composer_name));
} else if let Some(ref album_composer) = album.composer
&& let Some(ref composer_name) = album_composer.name
&& composer_name != "Various Composers"
&& !is_duplicate_composer(composer_name, &composer_normalized_set)
{
composers.push(composer_name.clone());
composer_normalized_set.insert(normalize_composer_name(composer_name));
}
} else {
if let Some(ref performers_str) = track.performers {
let extracted_composers = extract_composers_from_performers(performers_str);
for composer in extracted_composers {
if composer != "Various Composers"
&& !is_duplicate_composer(&composer, &composer_normalized_set)
{
composers.push(composer.clone());
composer_normalized_set.insert(normalize_composer_name(&composer));
}
}
}
if let Some(ref track_composer) = track.composer
&& let Some(ref composer_name) = track_composer.name
&& composer_name != "Various Composers"
&& !is_duplicate_composer(composer_name, &composer_normalized_set)
{
composers.push(composer_name.clone());
composer_normalized_set.insert(normalize_composer_name(composer_name));
}
if let Some(ref album_composer) = album.composer
&& let Some(ref composer_name) = album_composer.name
&& composer_name != "Various Composers"
&& !is_duplicate_composer(composer_name, &composer_normalized_set)
{
composers.push(composer_name.clone());
composer_normalized_set.insert(normalize_composer_name(composer_name));
}
}
let involved_people = if let Some(ref performers_str) = track.performers {
performers_str.clone()
} else {
String::new()
};
if config.involved_people && !involved_people.is_empty() {
let tag_item = TagItem::new(MusicianCredits, Text(involved_people));
tag.push(tag_item);
}
if config.composer && !composers.is_empty() {
let combined_composers = composers.join("/");
let tag_item = TagItem::new(Composer, Text(combined_composers));
tag.push(tag_item);
}
if config.label
&& let Some(ref album_label) = album.label
&& let Some(ref label_name) = album_label.name
{
let tag_item = TagItem::new(Label, Text(label_name.clone()));
tag.push(tag_item);
}
if config.genre
&& let Some(ref genre) = album.genre
&& let Some(ref genre_name) = genre.name
{
tag.set_genre(genre_name.clone());
}
if config.track_number
&& let Some(track_number) = track.track_number
{
tag.set_track(track_number as u32);
}
if config.track_total
&& let Some(ref album_tracks_count) = album.tracks_count
{
tag.set_track_total(*album_tracks_count as u32);
}
if config.disc_number
&& let Some(media_number) = track.media_number
{
tag.set_disk(media_number as u32);
}
if config.disc_total
&& let Some(ref album_media_count) = album.media_count
{
tag.set_disk_total(*album_media_count as u32);
}
if config.copyright
&& let Some(ref copyright) = track.copyright
{
let tag_item = TagItem::new(CopyrightMessage, Text(copyright.clone()));
tag.push(tag_item);
}
if config.isrc
&& let Some(ref isrc) = track.isrc
{
let tag_item = TagItem::new(Isrc, Text(isrc.clone()));
tag.push(tag_item);
}
let mut primary_date_full: Option<String> = None;
let mut primary_year: Option<u32> = None;
if let Some(ref release_date) = album.release_date_download {
primary_date_full = Some(release_date.clone());
if let Some(year_str) = release_date.split('-').next() {
primary_year = year_str.parse::<u32>().ok();
}
} else if let Some(ref release_date) = album.release_date_original {
primary_date_full = Some(release_date.clone());
if let Some(year_str) = release_date.split('-').next() {
primary_year = year_str.parse::<u32>().ok();
}
} else if let Some(ref release_date) = track.release_date_original {
primary_date_full = Some(release_date.clone());
if let Some(year_str) = release_date.split('-').next() {
primary_year = year_str.parse::<u32>().ok();
}
} else if let Some(released_at) = album.released_at {
let (date_str, year_num) = timestamp_to_date_and_year(released_at);
primary_date_full = date_str;
primary_year = year_num;
}
if config.release_year
&& let Some(year) = primary_year
{
tag.set_year(year);
}
if config.release_date {
if file_type == Flac
&& let Some(ref date_str) = primary_date_full
{
let tag_item = TagItem::new(RecordingDate, Text(date_str.clone()));
tag.push(tag_item);
}
if file_type == Mpeg
&& let Some(ref date_str) = primary_date_full
{
let tag_item = TagItem::new(ReleaseDate, Text(date_str.clone()));
tag.push(tag_item);
}
}
if config.url
&& let Some(ref product_url) = album.product_url
{
let full_url = if product_url.starts_with("http") {
product_url.clone()
} else {
format!("https://www.qobuz.com{}", product_url)
};
let tag_item = TagItem::new(CommercialInformationUrl, ItemValue::Locator(full_url));
tag.push(tag_item);
}
if config.media_type {
match file_type {
Flac => {
if let Some(ref release_type) = album.release_type {
let media_type_str = if release_type == "compilation" {
"compilation".to_string()
} else if release_type == "album" {
"album".to_string()
} else {
release_type.clone()
};
let tag_item = TagItem::new(OriginalMediaType, Text(media_type_str));
tag.push(tag_item);
} else if let Some(ref product_type) = album.product_type {
let media_type_str = if product_type == "compilation" {
"compilation".to_string()
} else if product_type == "album" {
"album".to_string()
} else {
product_type.clone()
};
let tag_item = TagItem::new(OriginalMediaType, Text(media_type_str));
tag.push(tag_item);
}
}
_ => {
if let Some(ref release_type) = album.release_type {
let tag_item = TagItem::new(OriginalMediaType, Text(release_type.clone()));
tag.push(tag_item);
} else if let Some(ref product_type) = album.product_type {
let tag_item = TagItem::new(OriginalMediaType, Text(product_type.clone()));
tag.push(tag_item);
}
}
}
}
if config.cover_art
&& let Some(ref album_image) = album.image
{
let image_url = album_image
.mega
.as_ref()
.or(album_image.extralarge.as_ref())
.or(album_image.large.as_ref())
.or(album_image.medium.as_ref())
.or(album_image.small.as_ref())
.or(album_image.thumbnail.as_ref());
if let Some(url) = image_url {
match download_image(url).await {
Ok(image_data) => {
let picture = Picture::new_unchecked(
CoverFront, Some(Jpeg), Some("".to_string()), image_data,
);
tag.push_picture(picture);
}
Err(e) => {
eprintln!(
"Warning: Could not download album cover from URL: {} - {}",
url, e
);
}
}
} else {
eprintln!("Warning: No album cover image URL available");
}
}
let options = WriteOptions::default();
tagged_file
.save_to_path(filepath, options)
.map_err(LoftyError)?;
Ok(())
}
fn extract_composers_from_performers(performers_str: &str) -> Vec<String> {
let mut composers = Vec::new();
let person_groups: Vec<&str> = performers_str.split(" - ").collect();
for group in person_groups.iter() {
let group = group.trim();
let mut parts: Vec<&str> = group.split(',').map(|s| s.trim()).collect();
if !parts.is_empty() {
let person_name = parts.remove(0).trim();
for role in &parts {
if role.contains("Composer") || role.contains("Lyricist") {
if !composers.contains(&person_name.to_string()) {
composers.push(person_name.to_string());
}
break; }
}
}
}
composers
}
fn extract_artist_names_from_performers(
performers_str: &str,
existing_artists: &HashSet<String>,
) -> Vec<String> {
let mut artist_names = Vec::new();
let person_groups: Vec<&str> = performers_str.split(" - ").collect();
for group in person_groups.iter() {
let group = group.trim();
let mut parts: Vec<&str> = group.split(',').map(|s| s.trim()).collect();
if !parts.is_empty() {
let person_name = parts.remove(0).trim();
let has_performer_role = parts.iter().any(|role| {
role.contains("MainArtist")
|| role.contains("Performer")
|| role.contains("AssociatedPerformer")
|| role.contains("Orchestra")
|| role.contains("Conductor")
});
if has_performer_role
&& !existing_artists.contains(person_name)
&& !artist_names.contains(&person_name.to_string())
{
artist_names.push(person_name.to_string());
}
}
}
artist_names
}
fn extract_producers_from_performers(performers_str: &str) -> Vec<String> {
let mut producers = Vec::new();
let person_groups: Vec<&str> = performers_str.split(" - ").collect();
for group in person_groups.iter() {
let group = group.trim();
let mut parts: Vec<&str> = group.split(',').map(|s| s.trim()).collect();
if !parts.is_empty() {
let person_name = parts.remove(0).trim();
if parts.iter().any(|role| role.contains("Producer")) {
producers.push(person_name.to_string());
}
}
}
producers
}
fn normalize_composer_name(name: &str) -> String {
let mut normalized = name
.to_lowercase()
.trim()
.replace(".", "")
.replace(",", "")
.replace("-", " ") .replace(" ", " ") .replace(" ", " ") .trim()
.to_string();
normalized = normalized
.replace("de homem -christo", "de homem christo")
.replace("de homem- christo", "de homem christo")
.replace("de homem - christo", "de homem christo");
normalized = normalized
.replace("guy manuel", "guymanuel")
.replace("guy-manuel", "guymanuel");
normalized = normalized
.replace("m. davis", "miles davis")
.replace("m davis", "miles davis");
normalized.trim().to_string()
}
fn is_duplicate_composer(composer_name: &str, existing_normalized_set: &HashSet<String>) -> bool {
let normalized_name = normalize_composer_name(composer_name);
if existing_normalized_set.contains(&normalized_name) {
return true;
}
for existing in existing_normalized_set {
if existing.contains(&normalized_name) || normalized_name.contains(existing) {
return true;
}
}
false
}