use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ReleaseKey {
MusicBrainz(String),
Titled(String, String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlbumLabel {
Directory(PathBuf),
Release {
artist: Option<String>,
album: String,
},
}
impl AlbumLabel {
pub fn from_tags(tags: &AlbumTags) -> Option<Self> {
Some(Self::Release {
artist: tags.effective_artist().map(str::to_string),
album: tags.album.clone()?,
})
}
}
impl fmt::Display for AlbumLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Directory(dir) => write!(f, "{}", dir.display()),
Self::Release {
artist: Some(artist),
album,
} => write!(f, "{} / {}", artist, album),
Self::Release {
artist: None,
album,
} => write!(f, "{}", album),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct AlbumTags {
pub album: Option<String>,
pub album_artist: Option<String>,
pub artist: Option<String>,
pub musicbrainz_album_id: Option<String>,
pub disc: Option<u64>,
pub track: Option<u64>,
}
impl AlbumTags {
pub fn effective_artist(&self) -> Option<&str> {
self.album_artist.as_deref().or(self.artist.as_deref())
}
pub fn has_album(&self) -> bool {
self.album.is_some()
}
pub fn release_key(&self) -> Option<ReleaseKey> {
let album = self.album.clone()?;
Some(match &self.musicbrainz_album_id {
Some(id) => ReleaseKey::MusicBrainz(id.clone()),
None => ReleaseKey::Titled(
self.effective_artist().unwrap_or_default().to_string(),
album,
),
})
}
}
pub fn repeated_position<'a>(
tags: impl IntoIterator<Item = &'a AlbumTags>,
) -> Option<(Option<u64>, u64, usize)> {
let mut seen: std::collections::HashMap<(Option<u64>, u64), usize> =
std::collections::HashMap::new();
for t in tags {
if let Some(track) = t.track {
*seen.entry((t.disc, track)).or_insert(0) += 1;
}
}
seen.into_iter()
.max_by_key(|(_, count)| *count)
.filter(|(_, count)| *count > 1)
.map(|((disc, track), count)| (disc, track, count))
}
#[cfg(feature = "replaygain")]
pub fn read_album_tags(path: &Path) -> Option<AlbumTags> {
use symphonia::core::formats::probe::Hint;
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::{MetadataOptions, StandardTag};
let file = std::fs::File::open(path).ok()?;
let mss = MediaSourceStream::new(Box::new(file), Default::default());
let mut hint = Hint::new();
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
hint.with_extension(ext);
}
let mut format = symphonia::default::get_probe()
.probe(&hint, mss, Default::default(), MetadataOptions::default())
.ok()?;
let mut tags = AlbumTags::default();
let mut metadata = format.metadata();
loop {
if let Some(revision) = metadata.current() {
for tag in &revision.media.tags {
match &tag.std {
Some(StandardTag::Album(v)) => fill(&mut tags.album, v),
Some(StandardTag::AlbumArtist(v)) => fill(&mut tags.album_artist, v),
Some(StandardTag::Artist(v)) => fill(&mut tags.artist, v),
Some(StandardTag::MusicBrainzAlbumId(v)) => {
fill(&mut tags.musicbrainz_album_id, v)
}
Some(StandardTag::DiscNumber(n)) => tags.disc = tags.disc.or(Some(*n)),
Some(StandardTag::TrackNumber(n)) => tags.track = tags.track.or(Some(*n)),
_ => {}
}
}
}
if metadata.pop().is_none() {
break;
}
}
Some(tags)
}
#[cfg(feature = "replaygain")]
fn fill(slot: &mut Option<String>, value: &str) {
if slot.is_some() {
return;
}
let trimmed = value.trim();
if !trimmed.is_empty() {
*slot = Some(trimmed.to_string());
}
}
#[cfg(not(feature = "replaygain"))]
pub fn read_album_tags(_path: &Path) -> Option<AlbumTags> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn album_artist_wins_over_artist() {
let tags = AlbumTags {
album_artist: Some("Various Artists".into()),
artist: Some("Pink Floyd".into()),
..Default::default()
};
assert_eq!(tags.effective_artist(), Some("Various Artists"));
}
#[test]
fn artist_is_the_fallback() {
let tags = AlbumTags {
artist: Some("Pink Floyd".into()),
..Default::default()
};
assert_eq!(tags.effective_artist(), Some("Pink Floyd"));
}
}