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, SpotifyUri};
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: SpotifyUri,
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: SpotifyUri,
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 = &SpotifyUri> {
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 = &SpotifyUri> {
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 = &SpotifyUri> {
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 = &SpotifyUri> {
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_uri: &SpotifyUri) -> RequestResult {
175        let SpotifyUri::Artist { .. } = artist_uri else {
176            return Err(Error::invalid_argument("artist_uri"));
177        };
178
179        session.spclient().get_artist_metadata(artist_uri).await
180    }
181
182    fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result<Self, Error> {
183        Self::try_from(msg)
184    }
185}
186
187impl TryFrom<&<Self as Metadata>::Message> for Artist {
188    type Error = librespot_core::Error;
189    fn try_from(artist: &<Self as Metadata>::Message) -> Result<Self, Self::Error> {
190        Ok(Self {
191            id: artist.try_into()?,
192            name: artist.name().to_owned(),
193            popularity: artist.popularity(),
194            top_tracks: artist.top_track.as_slice().try_into()?,
195            albums: artist.album_group.as_slice().try_into()?,
196            singles: artist.single_group.as_slice().try_into()?,
197            compilations: artist.compilation_group.as_slice().try_into()?,
198            appears_on_albums: artist.appears_on_group.as_slice().try_into()?,
199            external_ids: artist.external_id.as_slice().into(),
200            portraits: artist.portrait.as_slice().into(),
201            biographies: artist.biography.as_slice().into(),
202            activity_periods: artist.activity_period.as_slice().try_into()?,
203            restrictions: artist.restriction.as_slice().into(),
204            related: artist.related.as_slice().try_into()?,
205            is_portrait_album_cover: artist.is_portrait_album_cover(),
206            portrait_group: artist
207                .portrait_group
208                .get_or_default()
209                .image
210                .as_slice()
211                .into(),
212            sales_periods: artist.sale_period.as_slice().try_into()?,
213            availabilities: artist.availability.as_slice().try_into()?,
214        })
215    }
216}
217
218impl_try_from_repeated!(<Artist as Metadata>::Message, Artists);
219
220impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole {
221    type Error = librespot_core::Error;
222    fn try_from(artist_with_role: &ArtistWithRoleMessage) -> Result<Self, Self::Error> {
223        Ok(Self {
224            id: artist_with_role.try_into()?,
225            name: artist_with_role.artist_name().to_owned(),
226            role: artist_with_role.role(),
227        })
228    }
229}
230
231impl_try_from_repeated!(ArtistWithRoleMessage, ArtistsWithRole);
232
233impl TryFrom<&TopTracksMessage> for TopTracks {
234    type Error = librespot_core::Error;
235    fn try_from(top_tracks: &TopTracksMessage) -> Result<Self, Self::Error> {
236        Ok(Self {
237            country: top_tracks.country().to_owned(),
238            tracks: top_tracks.track.as_slice().try_into()?,
239        })
240    }
241}
242
243impl_try_from_repeated!(TopTracksMessage, CountryTopTracks);
244
245impl TryFrom<&AlbumGroupMessage> for AlbumGroup {
246    type Error = librespot_core::Error;
247    fn try_from(album_groups: &AlbumGroupMessage) -> Result<Self, Self::Error> {
248        Ok(Self(album_groups.album.as_slice().try_into()?))
249    }
250}
251
252impl AlbumGroups {
253    /// Get the contained albums. This will only use the latest release / variant of an album if
254    /// multiple variants are available. This should be used if multiple variants of the same album
255    /// are not explicitely desired.
256    pub fn current_releases(&self) -> impl Iterator<Item = &SpotifyUri> {
257        self.iter().filter_map(|agrp| agrp.first())
258    }
259}
260
261impl_try_from_repeated!(AlbumGroupMessage, AlbumGroups);
262
263impl From<&BiographyMessage> for Biography {
264    fn from(biography: &BiographyMessage) -> Self {
265        let portrait_group = biography
266            .portrait_group
267            .iter()
268            .map(|it| it.image.as_slice().into())
269            .collect();
270
271        Self {
272            text: biography.text().to_owned(),
273            portraits: biography.portrait.as_slice().into(),
274            portrait_group,
275        }
276    }
277}
278
279impl_from_repeated!(BiographyMessage, Biographies);
280
281impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod {
282    type Error = librespot_core::Error;
283
284    fn try_from(period: &ActivityPeriodMessage) -> Result<Self, Self::Error> {
285        let activity_period = match (
286            period.has_decade(),
287            period.has_start_year(),
288            period.has_end_year(),
289        ) {
290            // (decade, start_year, end_year)
291            (true, false, false) => Self::Decade(period.decade().try_into()?),
292            (false, true, closed_period) => Self::Timespan {
293                start_year: period.start_year().try_into()?,
294                end_year: closed_period
295                    .then(|| period.end_year().try_into())
296                    .transpose()?,
297            },
298            _ => {
299                return Err(librespot_core::Error::failed_precondition(
300                    "ActivityPeriod is expected to be either a decade or timespan",
301                ));
302            }
303        };
304        Ok(activity_period)
305    }
306}
307
308impl_try_from_repeated!(ActivityPeriodMessage, ActivityPeriods);