use std::{
fmt::Debug,
ops::{Deref, DerefMut},
};
use crate::{
Metadata,
request::RequestResult,
util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated},
};
use super::{
attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList,
permission::Capabilities,
};
use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId};
use librespot_protocol as protocol;
use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
#[derive(Debug, Clone, Default)]
pub struct Geoblocks(Vec<Geoblock>);
impl_deref_wrapped!(Geoblocks, Vec<Geoblock>);
#[derive(Debug, Clone)]
pub struct Playlist {
pub id: SpotifyUri,
pub revision: Vec<u8>,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: Option<PlaylistDiff>,
pub sync_result: Option<PlaylistDiff>,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub has_abuse_reporting: bool,
pub capabilities: Capabilities,
pub geoblocks: Geoblocks,
}
#[derive(Debug, Clone, Default)]
pub struct Playlists(pub Vec<SpotifyId>);
impl_deref_wrapped!(Playlists, Vec<SpotifyId>);
#[derive(Debug, Clone)]
pub struct SelectedListContent {
pub revision: Vec<u8>,
pub length: i32,
pub attributes: PlaylistAttributes,
pub contents: PlaylistItemList,
pub diff: Option<PlaylistDiff>,
pub sync_result: Option<PlaylistDiff>,
pub resulting_revisions: Playlists,
pub has_multiple_heads: bool,
pub is_up_to_date: bool,
pub nonces: Vec<i64>,
pub timestamp: Date,
pub owner_username: String,
pub has_abuse_reporting: bool,
pub capabilities: Capabilities,
pub geoblocks: Geoblocks,
}
impl Playlist {
pub fn tracks(&self) -> impl ExactSizeIterator<Item = &SpotifyUri> {
let tracks = self.contents.items.iter().map(|item| &item.id);
let length = tracks.len();
let expected_length = self.length as usize;
if length != expected_length {
warn!("Got {length} tracks, but the list should contain {expected_length} tracks.",);
}
tracks
}
pub fn name(&self) -> &str {
&self.attributes.name
}
}
#[async_trait]
impl Metadata for Playlist {
type Message = protocol::playlist4_external::SelectedListContent;
async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult {
let SpotifyUri::Playlist {
id: playlist_id, ..
} = playlist_uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
session.spclient().get_playlist(playlist_id).await
}
fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result<Self, Error> {
let SpotifyUri::Playlist {
id: playlist_id, ..
} = uri
else {
return Err(Error::invalid_argument("playlist_uri"));
};
let playlist = SelectedListContent::try_from(msg)?;
let new_uri = SpotifyUri::Playlist {
id: *playlist_id,
user: Some(playlist.owner_username),
};
Ok(Self {
id: new_uri,
revision: playlist.revision,
length: playlist.length,
attributes: playlist.attributes,
contents: playlist.contents,
diff: playlist.diff,
sync_result: playlist.sync_result,
resulting_revisions: playlist.resulting_revisions,
has_multiple_heads: playlist.has_multiple_heads,
is_up_to_date: playlist.is_up_to_date,
nonces: playlist.nonces,
timestamp: playlist.timestamp,
has_abuse_reporting: playlist.has_abuse_reporting,
capabilities: playlist.capabilities,
geoblocks: playlist.geoblocks,
})
}
}
impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
type Error = librespot_core::Error;
fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
let timestamp = playlist.timestamp();
let timestamp = if timestamp > 9295169800000 {
warn!("timestamp is very large; assuming it's in microseconds");
timestamp / 1000
} else {
timestamp
};
let timestamp = Date::from_timestamp_ms(timestamp)?;
Ok(Self {
revision: playlist.revision().to_owned(),
length: playlist.length(),
attributes: playlist.attributes.get_or_default().try_into()?,
contents: playlist.contents.get_or_default().try_into()?,
diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?,
sync_result: playlist
.sync_result
.as_ref()
.map(TryInto::try_into)
.transpose()?,
resulting_revisions: Playlists(
playlist
.resulting_revisions
.iter()
.map(|p| p.try_into())
.collect::<Result<Vec<SpotifyId>, Error>>()?,
),
has_multiple_heads: playlist.multiple_heads(),
is_up_to_date: playlist.up_to_date(),
nonces: playlist.nonces.clone(),
timestamp,
owner_username: playlist.owner_username().to_owned(),
has_abuse_reporting: playlist.abuse_reporting_enabled(),
capabilities: playlist.capabilities.get_or_default().into(),
geoblocks: Geoblocks(
playlist
.geoblock
.iter()
.map(|b| b.enum_value_or_default())
.collect(),
),
})
}
}
impl_from_repeated_copy!(Geoblock, Geoblocks);
impl_try_from_repeated!(Vec<u8>, Playlists);