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