librespot_metadata/playlist/
list.rs

1use std::{
2    fmt::Debug,
3    ops::{Deref, DerefMut},
4};
5
6use crate::{
7    request::RequestResult,
8    util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated},
9    Metadata,
10};
11
12use super::{
13    attribute::PlaylistAttributes, diff::PlaylistDiff, item::PlaylistItemList,
14    permission::Capabilities,
15};
16
17use librespot_core::{
18    date::Date,
19    spotify_id::{NamedSpotifyId, SpotifyId},
20    Error, Session,
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!(
82                "Got {} tracks, but the list should contain {} tracks.",
83                length, expected_length,
84            );
85        }
86
87        tracks
88    }
89
90    pub fn name(&self) -> &str {
91        &self.attributes.name
92    }
93}
94
95#[async_trait]
96impl Metadata for Playlist {
97    type Message = protocol::playlist4_external::SelectedListContent;
98
99    async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult {
100        session.spclient().get_playlist(playlist_id).await
101    }
102
103    fn parse(msg: &Self::Message, id: &SpotifyId) -> Result<Self, Error> {
104        // the playlist proto doesn't contain the id so we decorate it
105        let playlist = SelectedListContent::try_from(msg)?;
106        let id = NamedSpotifyId::from_spotify_id(*id, &playlist.owner_username);
107
108        Ok(Self {
109            id,
110            revision: playlist.revision,
111            length: playlist.length,
112            attributes: playlist.attributes,
113            contents: playlist.contents,
114            diff: playlist.diff,
115            sync_result: playlist.sync_result,
116            resulting_revisions: playlist.resulting_revisions,
117            has_multiple_heads: playlist.has_multiple_heads,
118            is_up_to_date: playlist.is_up_to_date,
119            nonces: playlist.nonces,
120            timestamp: playlist.timestamp,
121            has_abuse_reporting: playlist.has_abuse_reporting,
122            capabilities: playlist.capabilities,
123            geoblocks: playlist.geoblocks,
124        })
125    }
126}
127
128impl TryFrom<&<Playlist as Metadata>::Message> for SelectedListContent {
129    type Error = librespot_core::Error;
130    fn try_from(playlist: &<Playlist as Metadata>::Message) -> Result<Self, Self::Error> {
131        let timestamp = playlist.timestamp();
132        let timestamp = if timestamp > 9295169800000 {
133            // timestamp is way out of range for milliseconds. Some seem to be in microseconds?
134            // Observed on playlists where:
135            //   format: "artist-mix-reader"
136            //   format_attributes {
137            //     key: "mediaListConfig"
138            //     value: "spotify:medialistconfig:artist-seed-mix:default_v18"
139            //   }
140            warn!("timestamp is very large; assuming it's in microseconds");
141            timestamp / 1000
142        } else {
143            timestamp
144        };
145        let timestamp = Date::from_timestamp_ms(timestamp)?;
146
147        Ok(Self {
148            revision: playlist.revision().to_owned(),
149            length: playlist.length(),
150            attributes: playlist.attributes.get_or_default().try_into()?,
151            contents: playlist.contents.get_or_default().try_into()?,
152            diff: playlist.diff.as_ref().map(TryInto::try_into).transpose()?,
153            sync_result: playlist
154                .sync_result
155                .as_ref()
156                .map(TryInto::try_into)
157                .transpose()?,
158            resulting_revisions: Playlists(
159                playlist
160                    .resulting_revisions
161                    .iter()
162                    .map(|p| p.try_into())
163                    .collect::<Result<Vec<SpotifyId>, Error>>()?,
164            ),
165            has_multiple_heads: playlist.multiple_heads(),
166            is_up_to_date: playlist.up_to_date(),
167            nonces: playlist.nonces.clone(),
168            timestamp,
169            owner_username: playlist.owner_username().to_owned(),
170            has_abuse_reporting: playlist.abuse_reporting_enabled(),
171            capabilities: playlist.capabilities.get_or_default().into(),
172            geoblocks: Geoblocks(
173                playlist
174                    .geoblock
175                    .iter()
176                    .map(|b| b.enum_value_or_default())
177                    .collect(),
178            ),
179        })
180    }
181}
182
183impl_from_repeated_copy!(Geoblock, Geoblocks);
184impl_try_from_repeated!(Vec<u8>, Playlists);