librespot_metadata/
artist.rs

1use std::{
2    fmt::Debug,
3    ops::{Deref, DerefMut},
4};
5
6use crate::{
7    Metadata,
8    album::Albums,
9    availability::Availabilities,
10    external_id::ExternalIds,
11    image::Images,
12    request::RequestResult,
13    restriction::Restrictions,
14    sale_period::SalePeriods,
15    track::Tracks,
16    util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated},
17};
18
19use librespot_core::{Error, Session, SpotifyId};
20
21use librespot_protocol as protocol;
22pub use protocol::metadata::artist_with_role::ArtistRole;
23
24use protocol::metadata::ActivityPeriod as ActivityPeriodMessage;
25use protocol::metadata::AlbumGroup as AlbumGroupMessage;
26use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage;
27use protocol::metadata::Biography as BiographyMessage;
28use protocol::metadata::TopTracks as TopTracksMessage;
29
30#[derive(Debug, Clone)]
31pub struct Artist {
32    pub id: SpotifyId,
33    pub name: String,
34    pub popularity: i32,
35    pub top_tracks: CountryTopTracks,
36    pub albums: AlbumGroups,
37    pub singles: AlbumGroups,
38    pub compilations: AlbumGroups,
39    pub appears_on_albums: AlbumGroups,
40    pub external_ids: ExternalIds,
41    pub portraits: Images,
42    pub biographies: Biographies,
43    pub activity_periods: ActivityPeriods,
44    pub restrictions: Restrictions,
45    pub related: Artists,
46    pub is_portrait_album_cover: bool,
47    pub portrait_group: Images,
48    pub sales_periods: SalePeriods,
49    pub availabilities: Availabilities,
50}
51
52#[derive(Debug, Clone, Default)]
53pub struct Artists(pub Vec<Artist>);
54
55impl_deref_wrapped!(Artists, Vec<Artist>);
56
57#[derive(Debug, Clone)]
58pub struct ArtistWithRole {
59    pub id: SpotifyId,
60    pub name: String,
61    pub role: ArtistRole,
62}
63
64#[derive(Debug, Clone, Default)]
65pub struct ArtistsWithRole(pub Vec<ArtistWithRole>);
66
67impl_deref_wrapped!(ArtistsWithRole, Vec<ArtistWithRole>);
68
69#[derive(Debug, Clone)]
70pub struct TopTracks {
71    pub country: String,
72    pub tracks: Tracks,
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct CountryTopTracks(pub Vec<TopTracks>);
77
78impl_deref_wrapped!(CountryTopTracks, Vec<TopTracks>);
79
80#[derive(Debug, Clone, Default)]
81pub struct AlbumGroup(pub Albums);
82
83impl_deref_wrapped!(AlbumGroup, Albums);
84
85/// `AlbumGroups` contains collections of album variants (different releases of the same album).
86/// Ignoring the wrapping types it is structured roughly like this:
87/// ```text
88/// AlbumGroups [
89///     [Album1], [Album2-relelease, Album2-older-release], [Album3]
90/// ]
91/// ```
92/// In most cases only the current variant of each album is needed. A list of every album in its
93/// current release variant can be obtained by using [`AlbumGroups::current_releases`]
94#[derive(Debug, Clone, Default)]
95pub struct AlbumGroups(pub Vec<AlbumGroup>);
96
97impl_deref_wrapped!(AlbumGroups, Vec<AlbumGroup>);
98
99#[derive(Debug, Clone)]
100pub struct Biography {
101    pub text: String,
102    pub portraits: Images,
103    pub portrait_group: Vec<Images>,
104}
105
106#[derive(Debug, Clone, Default)]
107pub struct Biographies(pub Vec<Biography>);
108
109impl_deref_wrapped!(Biographies, Vec<Biography>);
110
111#[derive(Debug, Clone)]
112pub enum ActivityPeriod {
113    Timespan {
114        start_year: u16,
115        end_year: Option<u16>,
116    },
117    Decade(u16),
118}
119
120#[derive(Debug, Clone, Default)]
121pub struct ActivityPeriods(pub Vec<ActivityPeriod>);
122
123impl_deref_wrapped!(ActivityPeriods, Vec<ActivityPeriod>);
124
125impl CountryTopTracks {
126    pub fn for_country(&self, country: &str) -> Tracks {
127        if let Some(country) = self.0.iter().find(|top_track| top_track.country == country) {
128            return country.tracks.clone();
129        }
130
131        if let Some(global) = self.0.iter().find(|top_track| top_track.country.is_empty()) {
132            return global.tracks.clone();
133        }
134
135        Tracks(vec![]) // none found
136    }
137}
138
139impl Artist {
140    /// Get the full list of albums, not containing duplicate variants of the same albums.
141    ///
142    /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
143    pub fn albums_current(&self) -> impl Iterator<Item = &SpotifyId> {
144        self.albums.current_releases()
145    }
146
147    /// Get the full list of singles, not containing duplicate variants of the same singles.
148    ///
149    /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
150    pub fn singles_current(&self) -> impl Iterator<Item = &SpotifyId> {
151        self.singles.current_releases()
152    }
153
154    /// Get the full list of compilations, not containing duplicate variants of the same
155    /// compilations.
156    ///
157    /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
158    pub fn compilations_current(&self) -> impl Iterator<Item = &SpotifyId> {
159        self.compilations.current_releases()
160    }
161
162    /// Get the full list of albums, not containing duplicate variants of the same albums.
163    ///
164    /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`]
165    pub fn appears_on_albums_current(&self) -> impl Iterator<Item = &SpotifyId> {
166        self.appears_on_albums.current_releases()
167    }
168}
169
170#[async_trait]
171impl Metadata for Artist {
172    type Message = protocol::metadata::Artist;
173
174    async fn request(session: &Session, artist_id: &SpotifyId) -> RequestResult {
175        session.spclient().get_artist_metadata(artist_id).await
176    }
177
178    fn parse(msg: &Self::Message, _: &SpotifyId) -> Result<Self, Error> {
179        Self::try_from(msg)
180    }
181}
182
183impl TryFrom<&<Self as Metadata>::Message> for Artist {
184    type Error = librespot_core::Error;
185    fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
186        Ok(Self {
187            id: artist.try_into()?,
188            name: artist.name().to_owned(),
189            popularity: artist.popularity(),
190            top_tracks: artist.top_track.as_slice().try_into()?,
191            albums: artist.album_group.as_slice().try_into()?,
192            singles: artist.single_group.as_slice().try_into()?,
193            compilations: artist.compilation_group.as_slice().try_into()?,
194            appears_on_albums: artist.appears_on_group.as_slice().try_into()?,
195            external_ids: artist.external_id.as_slice().into(),
196            portraits: artist.portrait.as_slice().into(),
197            biographies: artist.biography.as_slice().into(),
198            activity_periods: artist.activity_period.as_slice().try_into()?,
199            restrictions: artist.restriction.as_slice().into(),
200            related: artist.related.as_slice().try_into()?,
201            is_portrait_album_cover: artist.is_portrait_album_cover(),
202            portrait_group: artist
203                .portrait_group
204                .get_or_default()
205                .image
206                .as_slice()
207                .into(),
208            sales_periods: artist.sale_period.as_slice().try_into()?,
209            availabilities: artist.availability.as_slice().try_into()?,
210        })
211    }
212}
213
214impl_try_from_repeated!(<Artist as Metadata>::Message, Artists);
215
216impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
217    type Error = librespot_core::Error;
218    fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
219        Ok(Self {
220            id: artist_with_role.try_into()?,
221            name: artist_with_role.artist_name().to_owned(),
222            role: artist_with_role.role(),
223        })
224    }
225}
226
227impl_try_from_repeated!(ArtistWithRoleMessage, ArtistsWithRole);
228
229impl TryFrom<&TopTracksMessage> for TopTracks {
230    type Error = librespot_core::Error;
231    fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
232        Ok(Self {
233            country: top_tracks.country().to_owned(),
234            tracks: top_tracks.track.as_slice().try_into()?,
235        })
236    }
237}
238
239impl_try_from_repeated!(TopTracksMessage, CountryTopTracks);
240
241impl TryFrom<&AlbumGroupMessage> for AlbumGroup {
242    type Error = librespot_core::Error;
243    fn try_from(album_groups: &AlbumGroupMessage) -> Result<Self, Self::Error> {
244        Ok(Self(album_groups.album.as_slice().try_into()?))
245    }
246}
247
248impl AlbumGroups {
249    /// Get the contained albums. This will only use the latest release / variant of an album if
250    /// multiple variants are available. This should be used if multiple variants of the same album
251    /// are not explicitely desired.
252    pub fn current_releases(&self) -> impl Iterator<Item = &SpotifyId> {
253        self.iter().filter_map(|agrp| agrp.first())
254    }
255}
256
257impl_try_from_repeated!(AlbumGroupMessage, AlbumGroups);
258
259impl From<&BiographyMessage> for Biography {
260    fn from(biography: &BiographyMessage) -> Self {
261        let portrait_group = biography
262            .portrait_group
263            .iter()
264            .map(|it| it.image.as_slice().into())
265            .collect();
266
267        Self {
268            text: biography.text().to_owned(),
269            portraits: biography.portrait.as_slice().into(),
270            portrait_group,
271        }
272    }
273}
274
275impl_from_repeated!(BiographyMessage, Biographies);
276
277impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod {
278    type Error = librespot_core::Error;
279
280    fn try_from(period: &ActivityPeriodMessage) -> Result<Self, Self::Error> {
281        let activity_period = match (
282            period.has_decade(),
283            period.has_start_year(),
284            period.has_end_year(),
285        ) {
286            // (decade, start_year, end_year)
287            (true, false, false) => Self::Decade(period.decade().try_into()?),
288            (false, true, closed_period) => Self::Timespan {
289                start_year: period.start_year().try_into()?,
290                end_year: closed_period
291                    .then(|| period.end_year().try_into())
292                    .transpose()?,
293            },
294            _ => {
295                return Err(librespot_core::Error::failed_precondition(
296                    "ActivityPeriod is expected to be either a decade or timespan",
297                ));
298            }
299        };
300        Ok(activity_period)
301    }
302}
303
304impl_try_from_repeated!(ActivityPeriodMessage, ActivityPeriods);