librespot_metadata/playlist/
list.rs

1use 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        // the playlist proto doesn't contain the id so we decorate it
111        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            // timestamp is way out of range for milliseconds. Some seem to be in microseconds?
144            // Observed on playlists where:
145            //   format: "artist-mix-reader"
146            //   format_attributes {
147            //     key: "mediaListConfig"
148            //     value: "spotify:medialistconfig:artist-seed-mix:default_v18"
149            //   }
150            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);