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::{
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        // the playlist proto doesn't contain the id so we decorate it
102        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            // timestamp is way out of range for milliseconds. Some seem to be in microseconds?
131            // Observed on playlists where:
132            //   format: "artist-mix-reader"
133            //   format_attributes {
134            //     key: "mediaListConfig"
135            //     value: "spotify:medialistconfig:artist-seed-mix:default_v18"
136            //   }
137            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);