use crate::tag::TagType;
use std::borrow::Cow;
use std::fmt::{Debug, Display, Formatter};
use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StarRating {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
}
pub trait RatingProvider: Send + Sync {
fn supports_email(&self, email: &str) -> bool;
fn rate(&self, tag_type: TagType, rating: StarRating) -> u8;
fn convert_raw(&self, tag_type: TagType, rating: u8) -> StarRating;
}
static CUSTOM_PROVIDER: OnceLock<&'static dyn RatingProvider> = OnceLock::new();
pub fn set_custom_rating_provider<T>(provider: T)
where
T: RatingProvider + 'static,
{
assert!(
CUSTOM_PROVIDER.set(Box::leak(Box::new(provider))).is_ok(),
"Multiple calls to `set_custom_rating_provider()`"
);
}
fn custom_provider() -> &'static dyn RatingProvider {
CUSTOM_PROVIDER.get().map_or(DEFAULT_PROVIDER, |p| *p)
}
pub struct DefaultRatingProvider;
impl RatingProvider for DefaultRatingProvider {
fn supports_email(&self, _: &str) -> bool {
true
}
fn rate(&self, tag_type: TagType, rating: StarRating) -> u8 {
MUSICBEE_PROVIDER.rate(tag_type, rating)
}
fn convert_raw(&self, tag_type: TagType, rating: u8) -> StarRating {
MUSICBEE_PROVIDER.convert_raw(tag_type, rating)
}
}
static DEFAULT_PROVIDER: &'static dyn RatingProvider = &DefaultRatingProvider;
static MUSICBEE_PROVIDER: &'static dyn RatingProvider = &MusicBeeProvider;
static WMP_PROVIDER: &'static dyn RatingProvider = &WindowsMediaPlayerProvider;
static PICARD_PROVIDER: &'static dyn RatingProvider = &PicardProvider;
pub struct MusicBeeProvider;
impl RatingProvider for MusicBeeProvider {
fn supports_email(&self, email: &str) -> bool {
email == Popularimeter::MUSICBEE_EMAIL
}
fn rate(&self, tag_type: TagType, rating: StarRating) -> u8 {
match tag_type {
TagType::Id3v2 => match rating {
StarRating::One => 1,
StarRating::Two => 64,
StarRating::Three => 128,
StarRating::Four => 196,
StarRating::Five => 255,
},
_ => {
let stars = rating as u8;
stars.saturating_mul(20)
},
}
}
#[allow(clippy::match_overlapping_arm)]
fn convert_raw(&self, tag_type: TagType, rating: u8) -> StarRating {
match tag_type {
TagType::Id3v2 => match rating {
..=1 => StarRating::One,
..=64 => StarRating::Two,
..=128 => StarRating::Three,
..=196 => StarRating::Four,
..=255 => StarRating::Five,
},
_ => match rating {
..=20 => StarRating::One,
..=40 => StarRating::Two,
..=60 => StarRating::Three,
..=80 => StarRating::Four,
_ => StarRating::Five,
},
}
}
}
pub struct WindowsMediaPlayerProvider;
impl RatingProvider for WindowsMediaPlayerProvider {
fn supports_email(&self, email: &str) -> bool {
email == Popularimeter::WMP_EMAIL
}
fn rate(&self, _: TagType, rating: StarRating) -> u8 {
MusicBeeProvider.rate(TagType::Id3v2, rating)
}
fn convert_raw(&self, _: TagType, rating: u8) -> StarRating {
MusicBeeProvider.convert_raw(TagType::Id3v2, rating)
}
}
pub struct PicardProvider;
impl RatingProvider for PicardProvider {
fn supports_email(&self, email: &str) -> bool {
email == Popularimeter::PICARD_EMAIL
}
fn rate(&self, tag_type: TagType, rating: StarRating) -> u8 {
match tag_type {
TagType::Id3v2 => {
let stars = rating as u8;
stars.saturating_mul(51)
},
_ => {
let stars = rating as u8;
stars.saturating_mul(5)
},
}
}
#[allow(clippy::match_overlapping_arm)]
fn convert_raw(&self, tag_type: TagType, rating: u8) -> StarRating {
match tag_type {
TagType::Id3v2 => match rating {
..=51 => StarRating::One,
..=102 => StarRating::Two,
..=153 => StarRating::Three,
..=204 => StarRating::Four,
..=255 => StarRating::Five,
},
_ => match rating {
..=5 => StarRating::One,
..=10 => StarRating::Two,
..=15 => StarRating::Three,
..=20 => StarRating::Four,
_ => StarRating::Five,
},
}
}
}
#[derive(Clone)]
pub struct Popularimeter<'a> {
pub(crate) email: Option<Cow<'a, str>>,
pub(crate) rating_provider: &'static dyn RatingProvider,
pub rating: StarRating,
pub play_counter: u64,
}
impl Debug for Popularimeter<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Popularimeter")
.field("email", &self.email)
.field("rating", &self.rating)
.field("play_counter", &self.play_counter)
.finish()
}
}
impl<'a> Popularimeter<'a> {
const WMP_EMAIL: &'static str = "Windows Media Player 9 Series";
const MUSICBEE_EMAIL: &'static str = "MusicBee";
const PICARD_EMAIL: &'static str = "users@musicbrainz.org";
pub fn custom(email: impl Into<Cow<'a, str>>, rating: StarRating, play_counter: u64) -> Self {
Self {
email: Some(email.into()),
rating_provider: custom_provider(),
rating,
play_counter,
}
}
pub(crate) fn mapped(
email: impl Into<Cow<'a, str>>,
tag_type: TagType,
rate: u8,
play_counter: u64,
) -> Option<Self> {
let email = email.into();
let rating_provider;
match &*email {
Popularimeter::WMP_EMAIL => rating_provider = WMP_PROVIDER,
Popularimeter::MUSICBEE_EMAIL => rating_provider = MUSICBEE_PROVIDER,
Popularimeter::PICARD_EMAIL => rating_provider = PICARD_PROVIDER,
_ => {
rating_provider = custom_provider();
if !rating_provider.supports_email(&email) {
return None;
}
},
}
let star_rating = rating_provider.convert_raw(tag_type, rate);
Some(Self {
email: (!email.is_empty()).then_some(email),
rating_provider,
rating: star_rating,
play_counter,
})
}
pub fn email(&self) -> Option<&str> {
self.email.as_deref()
}
pub fn rating(&self) -> StarRating {
self.rating
}
pub(crate) fn mapped_value(&self, tag_type: TagType) -> u8 {
self.rating_provider.rate(tag_type, self.rating)
}
pub(crate) fn from_str(s: &str) -> Result<Self, ()> {
let mut parts = s.splitn(3, '|');
let email = parts.next().ok_or(())?;
let star_rating = parts.next().ok_or(())?;
let play_counter = parts
.next()
.ok_or(())
.and_then(|p| p.parse::<u64>().map_err(|_| ()))?;
let star_rating = match star_rating.parse().map_err(|_| ())? {
1 => StarRating::One,
2 => StarRating::Two,
3 => StarRating::Three,
4 => StarRating::Four,
5 => StarRating::Five,
_ => return Err(()),
};
let rating_provider;
match email {
Popularimeter::WMP_EMAIL => rating_provider = WMP_PROVIDER,
Popularimeter::MUSICBEE_EMAIL => rating_provider = MUSICBEE_PROVIDER,
Popularimeter::PICARD_EMAIL => rating_provider = PICARD_PROVIDER,
_ => {
rating_provider = custom_provider();
if !rating_provider.supports_email(email) {
return Err(());
}
},
}
Ok(Popularimeter {
email: (!email.is_empty()).then(|| Cow::Owned(email.to_owned())),
rating_provider,
rating: star_rating,
play_counter,
})
}
}
impl Popularimeter<'static> {
pub fn windows_media_player(rating: StarRating, play_counter: u64) -> Self {
Self {
email: Some(Cow::Borrowed(Self::WMP_EMAIL)),
rating_provider: WMP_PROVIDER,
rating,
play_counter,
}
}
pub fn musicbee(rating: StarRating, play_counter: u64) -> Self {
Self {
email: Some(Cow::Borrowed(Self::MUSICBEE_EMAIL)),
rating_provider: MUSICBEE_PROVIDER,
rating,
play_counter,
}
}
pub fn picard(rating: StarRating, play_counter: u64) -> Self {
Self {
email: Some(Cow::Borrowed(Self::PICARD_EMAIL)),
rating_provider: PICARD_PROVIDER,
rating,
play_counter,
}
}
}
impl Display for Popularimeter<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let email = self.email.as_deref().unwrap_or("");
write!(f, "{email}|{}|{}", self.rating as u8, self.play_counter)
}
}