pub mod mappings;
pub mod normalize;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::str::FromStr;
use crate::Result;
use crate::file::DynamicFileType;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub use normalize::TagSystem;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum StandardField {
Title,
Artist,
Album,
AlbumArtist,
TrackNumber,
TotalTracks,
DiscNumber,
TotalDiscs,
Date,
Year,
Genre,
Comment,
Description,
Composer,
Performer,
Conductor,
Lyricist,
Publisher,
Copyright,
EncodedBy,
Encoder,
Language,
Mood,
BPM,
ISRC,
Barcode,
CatalogNumber,
Label,
Compilation,
Lyrics,
Work,
Movement,
MovementCount,
MovementIndex,
SortTitle,
SortArtist,
SortAlbum,
SortAlbumArtist,
SortComposer,
ReplayGainTrackGain,
ReplayGainTrackPeak,
ReplayGainAlbumGain,
ReplayGainAlbumPeak,
MusicBrainzTrackId,
MusicBrainzAlbumId,
MusicBrainzArtistId,
MusicBrainzReleaseGroupId,
AcoustIdFingerprint,
AcoustIdId,
}
impl fmt::Display for StandardField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Title => "Title",
Self::Artist => "Artist",
Self::Album => "Album",
Self::AlbumArtist => "Album Artist",
Self::TrackNumber => "Track Number",
Self::TotalTracks => "Total Tracks",
Self::DiscNumber => "Disc Number",
Self::TotalDiscs => "Total Discs",
Self::Date => "Date",
Self::Year => "Year",
Self::Genre => "Genre",
Self::Comment => "Comment",
Self::Description => "Description",
Self::Composer => "Composer",
Self::Performer => "Performer",
Self::Conductor => "Conductor",
Self::Lyricist => "Lyricist",
Self::Publisher => "Publisher",
Self::Copyright => "Copyright",
Self::EncodedBy => "Encoded By",
Self::Encoder => "Encoder",
Self::Language => "Language",
Self::Mood => "Mood",
Self::BPM => "BPM",
Self::ISRC => "ISRC",
Self::Barcode => "Barcode",
Self::CatalogNumber => "Catalog Number",
Self::Label => "Label",
Self::Compilation => "Compilation",
Self::Lyrics => "Lyrics",
Self::Work => "Work",
Self::Movement => "Movement",
Self::MovementCount => "Movement Count",
Self::MovementIndex => "Movement Index",
Self::SortTitle => "Sort Title",
Self::SortArtist => "Sort Artist",
Self::SortAlbum => "Sort Album",
Self::SortAlbumArtist => "Sort Album Artist",
Self::SortComposer => "Sort Composer",
Self::ReplayGainTrackGain => "ReplayGain Track Gain",
Self::ReplayGainTrackPeak => "ReplayGain Track Peak",
Self::ReplayGainAlbumGain => "ReplayGain Album Gain",
Self::ReplayGainAlbumPeak => "ReplayGain Album Peak",
Self::MusicBrainzTrackId => "MusicBrainz Track Id",
Self::MusicBrainzAlbumId => "MusicBrainz Album Id",
Self::MusicBrainzArtistId => "MusicBrainz Artist Id",
Self::MusicBrainzReleaseGroupId => "MusicBrainz Release Group Id",
Self::AcoustIdFingerprint => "AcoustID Fingerprint",
Self::AcoustIdId => "AcoustID Id",
};
write!(f, "{}", name)
}
}
impl FromStr for StandardField {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let normalized = s
.to_lowercase()
.replace('_', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
match normalized.as_str() {
"title" => Ok(Self::Title),
"artist" => Ok(Self::Artist),
"album" => Ok(Self::Album),
"album artist" | "albumartist" => Ok(Self::AlbumArtist),
"track number" | "tracknumber" | "track" => Ok(Self::TrackNumber),
"total tracks" | "totaltracks" | "tracktotal" => Ok(Self::TotalTracks),
"disc number" | "discnumber" | "disc" => Ok(Self::DiscNumber),
"total discs" | "totaldiscs" | "disctotal" => Ok(Self::TotalDiscs),
"date" => Ok(Self::Date),
"year" => Ok(Self::Year),
"genre" => Ok(Self::Genre),
"comment" => Ok(Self::Comment),
"description" => Ok(Self::Description),
"composer" => Ok(Self::Composer),
"performer" => Ok(Self::Performer),
"conductor" => Ok(Self::Conductor),
"lyricist" => Ok(Self::Lyricist),
"publisher" => Ok(Self::Publisher),
"copyright" => Ok(Self::Copyright),
"encoded by" | "encodedby" => Ok(Self::EncodedBy),
"encoder" => Ok(Self::Encoder),
"language" => Ok(Self::Language),
"mood" => Ok(Self::Mood),
"bpm" => Ok(Self::BPM),
"isrc" => Ok(Self::ISRC),
"barcode" => Ok(Self::Barcode),
"catalog number" | "catalognumber" => Ok(Self::CatalogNumber),
"label" => Ok(Self::Label),
"compilation" => Ok(Self::Compilation),
"lyrics" => Ok(Self::Lyrics),
"work" => Ok(Self::Work),
"movement" | "movementname" => Ok(Self::Movement),
"movement count" | "movementcount" => Ok(Self::MovementCount),
"movement index" | "movementindex" | "movement number" => Ok(Self::MovementIndex),
"sort title" | "sorttitle" => Ok(Self::SortTitle),
"sort artist" | "sortartist" => Ok(Self::SortArtist),
"sort album" | "sortalbum" => Ok(Self::SortAlbum),
"sort album artist" | "sortalbumartist" => Ok(Self::SortAlbumArtist),
"sort composer" | "sortcomposer" => Ok(Self::SortComposer),
"replaygain track gain" => Ok(Self::ReplayGainTrackGain),
"replaygain track peak" => Ok(Self::ReplayGainTrackPeak),
"replaygain album gain" => Ok(Self::ReplayGainAlbumGain),
"replaygain album peak" => Ok(Self::ReplayGainAlbumPeak),
"musicbrainz track id" | "musicbrainz trackid" => Ok(Self::MusicBrainzTrackId),
"musicbrainz album id" | "musicbrainz albumid" => Ok(Self::MusicBrainzAlbumId),
"musicbrainz artist id" | "musicbrainz artistid" => Ok(Self::MusicBrainzArtistId),
"musicbrainz release group id" | "musicbrainz releasegroupid" => {
Ok(Self::MusicBrainzReleaseGroupId)
}
"acoustid fingerprint" => Ok(Self::AcoustIdFingerprint),
"acoustid id" => Ok(Self::AcoustIdId),
_ => Err(format!("Unknown standard field: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TagMapPicture {
pub data: Vec<u8>,
pub mime_type: String,
pub picture_type: u8,
pub description: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TagMap {
fields: HashMap<StandardField, Vec<String>>,
custom: HashMap<String, Vec<String>>,
pictures: Vec<TagMapPicture>,
}
impl TagMap {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
custom: HashMap::new(),
pictures: Vec::new(),
}
}
pub fn get(&self, field: &StandardField) -> Option<&[String]> {
self.fields.get(field).map(|v| v.as_slice())
}
pub fn get_custom(&self, key: &str) -> Option<&[String]> {
self.custom.get(key).map(|v| v.as_slice())
}
pub fn set(&mut self, field: StandardField, values: Vec<String>) {
if values.is_empty() {
self.fields.remove(&field);
} else {
self.fields.insert(field, values);
}
}
pub fn set_custom(&mut self, key: String, values: Vec<String>) {
if values.is_empty() {
self.custom.remove(&key);
} else {
self.custom.insert(key, values);
}
}
pub fn remove(&mut self, field: &StandardField) {
self.fields.remove(field);
}
pub fn remove_custom(&mut self, key: &str) {
self.custom.remove(key);
}
pub fn standard_fields(&self) -> Vec<(&StandardField, &[String])> {
self.fields.iter().map(|(k, v)| (k, v.as_slice())).collect()
}
pub fn custom_fields(&self) -> Vec<(&str, &[String])> {
self.custom
.iter()
.map(|(k, v)| (k.as_str(), v.as_slice()))
.collect()
}
pub fn merge(&mut self, other: &TagMap, overwrite: bool) {
for (field, values) in &other.fields {
if overwrite || !self.fields.contains_key(field) {
self.fields.insert(field.clone(), values.clone());
}
}
for (key, values) in &other.custom {
if overwrite || !self.custom.contains_key(key) {
self.custom.insert(key.clone(), values.clone());
}
}
if overwrite {
if !other.pictures.is_empty() {
self.pictures = other.pictures.clone();
}
} else {
let mut seen: HashSet<TagMapPicture> = self.pictures.iter().cloned().collect();
for pic in &other.pictures {
if seen.insert(pic.clone()) {
self.pictures.push(pic.clone());
}
}
}
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty() && self.custom.is_empty() && self.pictures.is_empty()
}
pub fn clear(&mut self) {
self.fields.clear();
self.custom.clear();
self.pictures.clear();
}
}
impl Default for TagMap {
fn default() -> Self {
Self::new()
}
}
pub trait IntoTagMap {
fn to_tag_map(&self) -> TagMap;
}
pub trait FromTagMap {
fn apply_tag_map(&mut self, map: &TagMap) -> Result<ConversionReport>;
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ConversionReport {
pub transferred: Vec<StandardField>,
pub custom_transferred: Vec<String>,
pub skipped: Vec<(String, SkipReason)>,
pub warnings: Vec<String>,
}
impl fmt::Display for ConversionReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"Transferred: {} standard, {} custom",
self.transferred.len(),
self.custom_transferred.len()
)?;
if !self.skipped.is_empty() {
writeln!(f, "Skipped: {}", self.skipped.len())?;
for (field, reason) in &self.skipped {
writeln!(f, " {} — {}", field, reason)?;
}
}
if !self.warnings.is_empty() {
writeln!(f, "Warnings:")?;
for warning in &self.warnings {
writeln!(f, " {}", warning)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum SkipReason {
UnsupportedByTarget,
ReadOnlyFormat,
ValueTooLong {
max_len: usize,
},
IncompatibleType,
}
impl fmt::Display for SkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedByTarget => write!(f, "unsupported by target format"),
Self::ReadOnlyFormat => write!(f, "target format is read-only"),
Self::ValueTooLong { max_len } => {
write!(f, "value too long (max {} bytes)", max_len)
}
Self::IncompatibleType => write!(f, "incompatible value type"),
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ConversionOptions {
pub include_fields: Option<HashSet<StandardField>>,
pub exclude_fields: HashSet<StandardField>,
pub transfer_custom: bool,
pub overwrite: bool,
pub clear_destination: bool,
}
impl Default for ConversionOptions {
fn default() -> Self {
Self {
include_fields: None,
exclude_fields: HashSet::new(),
transfer_custom: true,
overwrite: true,
clear_destination: false,
}
}
}
pub fn convert_tags(
source: &DynamicFileType,
dest: &mut DynamicFileType,
) -> Result<ConversionReport> {
info_event!("converting tags between formats");
let map = source.to_tag_map();
let report = dest.apply_tag_map(&map)?;
info_event!(
transferred = report.transferred.len(),
custom = report.custom_transferred.len(),
skipped = report.skipped.len(),
"tag conversion complete"
);
report
.warnings
.iter()
.for_each(|_w| warn_event!(warning = %_w, "conversion warning"));
Ok(report)
}
pub fn convert_tags_with_options(
source: &DynamicFileType,
dest: &mut DynamicFileType,
options: &ConversionOptions,
) -> Result<ConversionReport> {
info_event!("converting tags with options");
let mut map = source.to_tag_map();
if let Some(ref include) = options.include_fields {
map.fields.retain(|field, _| include.contains(field));
}
map.fields
.retain(|field, _| !options.exclude_fields.contains(field));
map.fields.retain(|_, values| !values.is_empty());
if !options.transfer_custom {
map.custom.clear();
}
if options.clear_destination {
for key in dest.keys() {
let _ = dest.remove(&key);
}
} else if !options.overwrite {
let existing = dest.to_tag_map();
map.fields
.retain(|field, _| !existing.fields.contains_key(field));
map.custom
.retain(|key, _| !existing.custom.contains_key(key));
}
map.custom.retain(|_, values| !values.is_empty());
let report = dest.apply_tag_map(&map)?;
info_event!(
transferred = report.transferred.len(),
custom = report.custom_transferred.len(),
skipped = report.skipped.len(),
"tag conversion with options complete"
);
Ok(report)
}