librespot_metadata/playlist/
list.rs1use std::{
2 fmt::Debug,
3 ops::{Deref, DerefMut},
4};
5
6use crate::{
7 Metadata,
8 request::RequestResult,
9 util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated},
10};
11
12use super::{
13 attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList,
14 permission::Capabilities,
15};
16
17use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId};
18use librespot_protocol as protocol;
19use protocol::playlist4_external::GeoblockBlockingType as Geoblock;
20
21#[derive(Debug, Clone, Default)]
22pub struct Geoblocks(Vec<Geoblock>);
23
24impl_deref_wrapped!(Geoblocks, Vec<Geoblock>);
25
26#[derive(Debug, Clone)]
27pub struct Playlist {
28 pub id: SpotifyUri,
29 pub revision: Vec<u8>,
30 pub length: i32,
31 pub attributes: PlaylistAttributes,
32 pub contents: PlaylistItemList,
33 pub diff: Option<PlaylistDiff>,
34 pub sync_result: Option<PlaylistDiff>,
35 pub resulting_revisions: Playlists,
36 pub has_multiple_heads: bool,
37 pub is_up_to_date: bool,
38 pub nonces: Vec<i64>,
39 pub timestamp: Date,
40 pub has_abuse_reporting: bool,
41 pub capabilities: Capabilities,
42 pub geoblocks: Geoblocks,
43}
44
45#[derive(Debug, Clone, Default)]
46pub struct Playlists(pub Vec<SpotifyId>);
47
48impl_deref_wrapped!(Playlists, Vec<SpotifyId>);
49
50#[derive(Debug, Clone)]
51pub struct SelectedListContent {
52 pub revision: Vec<u8>,
53 pub length: i32,
54 pub attributes: PlaylistAttributes,
55 pub contents: PlaylistItemList,
56 pub diff: Option<PlaylistDiff>,
57 pub sync_result: Option<PlaylistDiff>,
58 pub resulting_revisions: Playlists,
59 pub has_multiple_heads: bool,
60 pub is_up_to_date: bool,
61 pub nonces: Vec<i64>,
62 pub timestamp: Date,
63 pub owner_username: String,
64 pub has_abuse_reporting: bool,
65 pub capabilities: Capabilities,
66 pub geoblocks: Geoblocks,
67}
68
69impl Playlist {
70 pub fn tracks(&self) -> impl ExactSizeIterator<Item = &SpotifyUri> {
71 let tracks = self.contents.items.iter().map(|item| &item.id);
72
73 let length = tracks.len();
74 let expected_length = self.length as usize;
75 if length != expected_length {
76 warn!("Got {length} tracks, but the list should contain {expected_length} tracks.",);
77 }
78
79 tracks
80 }
81
82 pub fn name(&self) -> &str {
83 &self.attributes.name
84 }
85}
86
87#[async_trait]
88impl Metadata for Playlist {
89 type Message = protocol::playlist4_external::SelectedListContent;
90
91 async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult {
92 let SpotifyUri::Playlist {
93 id: playlist_id, ..
94 } = playlist_uri
95 else {
96 return Err(Error::invalid_argument("playlist_uri"));
97 };
98
99 session.spclient().get_playlist(playlist_id).await
100 }
101
102 fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result<Self, Error> {
103 let SpotifyUri::Playlist {
104 id: playlist_id, ..
105 } = uri
106 else {
107 return Err(Error::invalid_argument("playlist_uri"));
108 };
109
110 let playlist = SelectedListContent::try_from(msg)?;
112
113 let new_uri = SpotifyUri::Playlist {
114 id: *playlist_id,
115 user: Some(playlist.owner_username),
116 };
117
118 Ok(Self {
119 id: new_uri,
120 revision: playlist.revision,
121 length: playlist.length,
122 attributes: playlist.attributes,
123 contents: playlist.contents,
124 diff: playlist.diff,
125 sync_result: playlist.sync_result,
126 resulting_revisions: playlist.resulting_revisions,
127 has_multiple_heads: playlist.has_multiple_heads,
128 is_up_to_date: playlist.is_up_to_date,
129 nonces: playlist.nonces,
130 timestamp: playlist.timestamp,
131 has_abuse_reporting: playlist.has_abuse_reporting,
132 capabilities: playlist.capabilities,
133 geoblocks: playlist.geoblocks,
134 })
135 }
136}
137
138impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
139 type Error = librespot_core::Error;
140 fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
141 let timestamp = playlist.timestamp();
142 let timestamp = if timestamp > 9295169800000 {
143 warn!("timestamp is very large; assuming it's in microseconds");
151 timestamp / 1000
152 } else {
153 timestamp
154 };
155 let timestamp = Date::from_timestamp_ms(timestamp)?;
156
157 Ok(Self {
158 revision: playlist.revision().to_owned(),
159 length: playlist.length(),
160 attributes: playlist.attributes.get_or_default().try_into()?,
161 contents: playlist.contents.get_or_default().try_into()?,
162 diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?,
163 sync_result: playlist
164 .sync_result
165 .as_ref()
166 .map(TryInto::try_into)
167 .transpose()?,
168 resulting_revisions: Playlists(
169 playlist
170 .resulting_revisions
171 .iter()
172 .map(|p| p.try_into())
173 .collect::<Result<Vec<SpotifyId>, Error>>()?,
174 ),
175 has_multiple_heads: playlist.multiple_heads(),
176 is_up_to_date: playlist.up_to_date(),
177 nonces: playlist.nonces.clone(),
178 timestamp,
179 owner_username: playlist.owner_username().to_owned(),
180 has_abuse_reporting: playlist.abuse_reporting_enabled(),
181 capabilities: playlist.capabilities.get_or_default().into(),
182 geoblocks: Geoblocks(
183 playlist
184 .geoblock
185 .iter()
186 .map(|b| b.enum_value_or_default())
187 .collect(),
188 ),
189 })
190 }
191}
192
193impl_from_repeated_copy!(Geoblock, Geoblocks);
194impl_try_from_repeated!(Vec<u8>, Playlists);