pub mod episodes;
pub mod livestream;
pub mod songs;
pub use episodes::EpisodeData;
pub use livestream::LivestreamData;
pub use songs::SongData;
use std::{
collections::HashMap,
ops::Deref,
time::{Duration, SystemTime},
};
use serde::{Deserialize, Serialize};
use serde_with::{
formats::Flexible, serde_as, DefaultOnError, DisplayFromStr, DurationSeconds, PickFirst,
TimestampSeconds,
};
use url::Url;
use veil::Redact;
use crate::track::TrackId;
use super::Method;
pub type Queue = Vec<ListData>;
#[serde_as]
#[derive(Clone, PartialEq, Deserialize, Serialize, Redact)]
#[serde(tag = "__TYPE__")]
pub enum ListData {
#[serde(rename = "song")]
Song {
#[serde(rename = "SNG_ID")]
#[serde_as(as = "PickFirst<(DisplayFromStr, _)>")]
id: TrackId,
#[serde(default)]
#[serde(rename = "ART_NAME")]
artist: String,
#[serde(default)]
#[serde(rename = "ALB_TITLE")]
album_title: String,
#[serde(default)]
#[serde(rename = "ALB_PICTURE")]
album_cover: String,
#[serde(default)]
#[serde(rename = "DURATION")]
#[serde_as(as = "DurationSeconds<String, Flexible>")]
duration: Duration,
#[serde(default)]
#[serde(rename = "SNG_TITLE")]
title: String,
#[serde(rename = "GAIN")]
#[serde_as(as = "Option<DisplayFromStr>")]
gain: Option<f64>,
#[serde(rename = "TRACK_TOKEN")]
#[redact]
track_token: String,
#[serde(rename = "TRACK_TOKEN_EXPIRE")]
#[serde_as(as = "TimestampSeconds<i64, Flexible>")]
expiry: SystemTime,
#[serde(rename = "FALLBACK")]
fallback: Option<Box<Self>>,
},
#[serde(rename = "episode")]
Episode {
#[serde(rename = "EPISODE_ID")]
#[serde_as(as = "PickFirst<(DisplayFromStr, _)>")]
id: TrackId,
#[serde(rename = "AVAILABLE")]
#[serde(default)]
available: bool,
#[serde(rename = "DURATION")]
#[serde_as(as = "DurationSeconds<String, Flexible>")]
duration: Duration,
#[serde(rename = "EPISODE_DIRECT_STREAM_URL")]
external_url: Option<Url>,
#[serde(default)]
#[serde(rename = "EPISODE_TITLE")]
title: String,
#[serde(default)]
#[serde(rename = "SHOW_IS_DIRECT_STREAM")]
#[serde(deserialize_with = "bool_from_string")]
external: bool,
#[serde(default)]
#[serde(rename = "SHOW_NAME")]
podcast_title: String,
#[serde(default)]
#[serde(rename = "SHOW_ART_MD5")]
podcast_art: String,
#[serde(rename = "TRACK_TOKEN")]
#[redact]
track_token: String,
#[serde(rename = "TRACK_TOKEN_EXPIRE")]
#[serde_as(as = "TimestampSeconds<i64, Flexible>")]
expiry: SystemTime,
},
#[serde(rename = "livestream")]
Livestream {
#[serde(rename = "LIVESTREAM_ID")]
#[serde_as(as = "PickFirst<(_, DisplayFromStr)>")]
id: TrackId,
#[serde(default)]
#[serde(rename = "LIVESTREAM_TITLE")]
title: String,
#[serde(default)]
#[serde(rename = "LIVESTREAM_IMAGE_MD5")]
live_stream_art: String,
#[serde(rename = "LIVESTREAM_URLS")]
#[serde_as(deserialize_as = "DefaultOnError")]
external_urls: LivestreamUrls,
#[serde(rename = "AVAILABLE")]
#[serde(default)]
available: bool,
},
}
fn bool_from_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"1" => Ok(true),
"0" => Ok(false),
_ => Err(serde::de::Error::custom("invalid boolean string")),
}
}
impl ListData {
#[must_use]
#[inline]
pub const fn typ(&self) -> &'static str {
match self {
ListData::Song { .. } => "song",
ListData::Episode { .. } => "episode",
ListData::Livestream { .. } => "livestream",
}
}
#[must_use]
#[inline]
pub fn id(&self) -> TrackId {
match self {
ListData::Song { id, .. }
| ListData::Episode { id, .. }
| ListData::Livestream { id, .. } => *id,
}
}
#[must_use]
#[inline]
pub fn title(&self) -> Option<&str> {
match self {
ListData::Song { title, .. } | ListData::Episode { title, .. } => Some(title.as_str()),
ListData::Livestream { .. } => None,
}
}
#[must_use]
#[inline]
pub fn artist(&self) -> &str {
match self {
ListData::Song { artist, .. } => artist.as_str(),
ListData::Episode { podcast_title, .. } => podcast_title.as_str(),
ListData::Livestream { title, .. } => title.as_str(),
}
}
#[must_use]
#[inline]
pub fn cover_id(&self) -> &str {
match self {
ListData::Song { album_cover, .. } => album_cover,
ListData::Episode { podcast_art, .. } => podcast_art,
ListData::Livestream {
live_stream_art, ..
} => live_stream_art,
}
}
#[must_use]
#[inline]
pub fn duration(&self) -> Option<Duration> {
match self {
ListData::Song { duration, .. } | ListData::Episode { duration, .. } => Some(*duration),
ListData::Livestream { .. } => None,
}
}
#[must_use]
#[inline]
pub fn track_token(&self) -> Option<&str> {
match self {
ListData::Song { track_token, .. } | ListData::Episode { track_token, .. } => {
Some(track_token)
}
ListData::Livestream { .. } => None,
}
}
#[must_use]
#[inline]
pub fn expiry(&self) -> Option<SystemTime> {
match self {
ListData::Song { expiry, .. } | ListData::Episode { expiry, .. } => Some(*expiry),
ListData::Livestream { .. } => None,
}
}
}
pub type LivestreamUrl = HashMap<String, CodecUrl>;
#[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct LivestreamUrls {
pub data: LivestreamUrl,
}
impl Deref for LivestreamUrls {
type Target = LivestreamUrl;
#[inline]
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl LivestreamUrls {
#[must_use]
pub fn sort_by_bitrate(&self) -> Vec<(usize, CodecUrl)> {
let mut entries: Vec<_> = self
.data
.iter()
.filter_map(|(bitrate, codec_url)| {
bitrate.parse::<usize>().ok().map(|num| (num, codec_url))
})
.collect();
entries.sort_by_key(|(bitrate, _)| *bitrate);
entries
.into_iter()
.map(|(bitrate, codec_url)| (bitrate, codec_url.clone()))
.collect()
}
}
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, Redact)]
#[redact(all)]
pub struct CodecUrl {
pub aac: Option<Url>,
pub mp3: Option<Url>,
}