spotifyio/crates/spotifyioweb/src/gql_model.rs

735 lines
19 KiB
Rust

//! Data model for the Spotify API
use std::num::NonZeroU32;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DefaultOnError, DisplayFromStr, VecSkipError};
use time::OffsetDateTime;
use spotifyio_model::{
AlbumId, ArtistId, ConcertId, PlaylistId, PrereleaseId, SongwriterId, TrackId, UserId,
UserlikeId,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LyricsWrap {
pub lyrics: Lyrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GqlWrap<T> {
pub data: T,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
#[allow(clippy::large_enum_variant)]
pub enum ArtistOption {
Artist(ArtistItem),
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
#[allow(clippy::large_enum_variant)]
pub enum AlbumOption {
Album(AlbumItem),
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
#[allow(clippy::large_enum_variant)]
pub enum TrackOption {
Track(TrackItem),
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
#[allow(clippy::large_enum_variant)]
pub enum PlaylistOption {
Playlist(GqlPlaylistItem),
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
#[allow(clippy::large_enum_variant)]
pub enum ConcertOption {
ConcertV2(ConcertGql),
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GqlPagination<T> {
pub items: Vec<T>,
pub total_count: Option<NonZeroU32>,
}
impl<T> Default for GqlPagination<T> {
fn default() -> Self {
Self {
items: Vec::new(),
total_count: None,
}
}
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Lyrics {
#[serde_as(as = "DefaultOnError")]
pub sync_type: Option<SyncType>,
pub lines: Vec<LyricsLine>,
pub provider: Option<String>,
pub provider_lyrics_id: Option<String>,
pub provider_display_name: Option<String>,
pub language: Option<String>,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum SyncType {
LineSynced,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LyricsLine {
#[serde_as(as = "Option<DisplayFromStr>")]
pub start_time_ms: Option<u32>,
pub words: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ArtistGqlWrap {
pub artist_union: ArtistGql,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ArtistGql {
pub uri: ArtistId<'static>,
pub profile: ArtistProfile,
pub related_content: Option<RelatedContent>,
pub stats: Option<ArtistStats>,
#[serde(default)]
pub visuals: Visuals,
pub pre_release: Option<PrereleaseItem>,
#[serde(default)]
pub goods: Goods,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ArtistProfile {
pub name: String,
#[serde(default)]
pub verified: bool,
#[serde(default)]
pub biography: Biography,
#[serde(default)]
pub external_links: GqlPagination<ExternalLink>,
#[serde(default)]
pub playlists_v2: GqlPagination<GqlWrap<PlaylistOption>>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Biography {
pub text: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ExternalLink {
pub name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ArtistStats {
pub followers: u32,
pub monthly_listeners: u32,
pub world_rank: u32,
pub top_cities: GqlPagination<City>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct City {
pub city: String,
pub country: String,
pub number_of_listeners: u32,
pub region: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
#[non_exhaustive]
pub struct Visuals {
pub avatar_image: Option<Image>,
pub gallery: GqlPagination<Image>,
pub header_image: Option<Image>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
#[non_exhaustive]
pub struct Goods {
pub events: Events,
pub merch: GqlPagination<MerchItem>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Events {
pub concerts: GqlPagination<Concert>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Concert {
pub uri: ConcertId<'static>,
pub title: String,
pub date: DateWrap,
#[serde(default)]
pub festival: bool,
pub partner_links: Option<GqlPagination<PartnerLink>>,
pub venue: Venue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PartnerLink {
pub partner_name: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Venue {
pub name: String,
pub location: Option<Location>,
pub coordinates: Option<Coordinates>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Location {
pub name: String,
pub city: Option<String>,
pub coordinates: Option<Coordinates>,
// ISO-3166 country code
pub country: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Coordinates {
pub latitude: f64,
pub longitude: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MerchItem {
pub uri: String,
pub url: String,
pub name: String,
pub price: String,
pub description: String,
pub image: Image,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Image {
pub sources: Vec<ImageSource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ImageSource {
pub url: String,
pub height: Option<u16>,
pub width: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Name {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct GqlPlaylistItem {
pub uri: PlaylistId<'static>,
pub name: String,
pub images: GqlPagination<Image>,
pub owner_v2: GqlWrap<UserItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UserItem {
pub uri: Option<UserId<'static>>,
#[serde(alias = "displayName")]
pub name: Option<String>,
pub avatar: Option<Image>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ArtistItem {
pub uri: ArtistId<'static>,
pub profile: Name,
pub visuals: Option<Visuals>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RelatedContent {
#[serde(default)]
pub discovered_on_v2: GqlPagination<GqlWrap<PlaylistOption>>,
#[serde(default)]
pub featuring_v2: GqlPagination<GqlWrap<PlaylistOption>>,
#[serde(default)]
pub related_artists: GqlPagination<ArtistItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PrereleaseLookup {
pub lookup: Vec<GqlWrap<PrereleaseItem>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PrereleaseItem {
/// URI of the prerelease
pub uri: PrereleaseId<'static>,
pub pre_release_content: PrereleaseContent,
pub release_date: DateWrap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PrereleaseContent {
/// URI of the to-be-released album
pub uri: Option<AlbumId<'static>>,
pub name: String,
pub cover_art: Option<Image>,
pub artists: Option<GqlPagination<GqlWrap<ArtistItem>>>,
pub tracks: Option<GqlPagination<PrereleaseTrackItem>>,
pub copyright: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PrereleaseTrackItem {
pub uri: TrackId<'static>,
pub name: String,
pub duration: Option<DurationWrap>,
pub artists: GqlPagination<GqlWrap<ArtistItem>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct DateWrap {
#[serde(with = "time::serde::iso8601")]
pub iso_string: OffsetDateTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct DateYear {
pub year: Option<u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct DurationWrap {
pub total_milliseconds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SearchResultWrap {
pub search_v2: GqlSearchResult,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum MatchedField {
Lyrics,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct GqlSearchTrackWrap {
pub item: GqlWrap<TrackOption>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub matched_fields: Vec<MatchedField>,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChipOrder {
#[serde_as(as = "VecSkipError<_>")]
pub items: Vec<Chip>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Chip {
type_name: SearchItemType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct GqlSearchResult {
#[serde(default)]
pub artists: GqlPagination<GqlWrap<ArtistOption>>,
#[serde(default)]
pub albums_v2: GqlPagination<AlbumItemWrap>,
#[serde(default)]
pub tracks_v2: GqlPagination<GqlSearchTrackWrap>,
#[serde(default)]
pub playlists: GqlPagination<GqlWrap<PlaylistOption>>,
#[serde(default)]
pub users: GqlPagination<GqlWrap<UserItem>>,
pub chip_order: ChipOrder,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "__typename")]
pub enum AlbumItemWrap {
AlbumResponseWrapper {
data: AlbumItem,
},
PreReleaseResponseWrapper {
data: PrereleaseItem,
},
#[serde(alias = "GenericError")]
NotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AlbumItem {
pub uri: AlbumId<'static>,
pub name: String,
pub date: Option<DateYear>,
pub cover_art: Option<Image>,
pub artists: Option<GqlPagination<ArtistItem>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct TrackItem {
pub uri: TrackId<'static>,
pub name: String,
pub duration: DurationWrap,
pub artists: GqlPagination<ArtistItem>,
pub album_of_track: AlbumItem,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ConcertGqlWrap {
pub concert: ConcertOption,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConcertGql {
pub uri: ConcertId<'static>,
pub title: String,
#[serde(default)]
pub artists: GqlPagination<GqlWrap<ArtistItem>>,
#[serde(default, with = "time::serde::iso8601::option")]
pub start_date_iso_string: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::iso8601::option")]
pub doors_open_time_iso_string: Option<OffsetDateTime>,
#[serde(default)]
pub festival: bool,
pub html_description: Option<String>,
pub location: Option<Location>,
#[serde(default)]
pub offers: GqlPagination<ConcertOffer>,
#[serde(default)]
pub related_concerts: GqlPagination<GqlWrap<ConcertOption>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConcertOffer {
pub access_code: String,
pub currency: Option<String>,
pub dates: Option<ConcertOfferDates>,
#[serde(default)]
pub first_party: bool,
pub max_price: Option<f32>,
pub min_price: Option<f32>,
pub provider_image_url: Option<String>,
pub provider_name: Option<String>,
pub sale_type: Option<String>,
pub sold_out: Option<bool>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ConcertOfferDates {
#[serde(with = "time::serde::iso8601")]
pub start_date_iso_string: OffsetDateTime,
#[serde(with = "time::serde::iso8601")]
pub end_date_iso_string: OffsetDateTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SearchItemType {
Artists,
Albums,
Tracks,
Playlists,
Users,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UserProfile {
pub uri: UserId<'static>,
pub name: Option<String>,
pub image_url: Option<String>,
pub followers_count: Option<u32>,
pub following_count: Option<u32>,
#[serde(default)]
pub public_playlists: Vec<PublicPlaylistItem>,
pub total_public_playlists_count: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UserPlaylists {
#[serde(default)]
pub public_playlists: Vec<PublicPlaylistItem>,
pub total_public_playlists_count: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PublicPlaylistItem {
pub uri: PlaylistId<'static>,
pub name: String,
pub owner_name: String,
pub owner_uri: UserId<'static>,
/// UID-based image id
///
/// - `spotify:image:ab67706c0000da8474fffd106bb7f5be3ba4b758`
/// - `spotify:mosaic:ab67616d00001e021c04efd2804b16cf689de7f0:ab67616d00001e0269f63a842ea91ca7c522593a:ab67616d00001e0270dbc9f47669d120ad874ec1:ab67616d00001e027d384516b23347e92a587ed1`
pub image_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UserProfilesWrap<T> {
pub profiles: Vec<T>,
}
/// May be an artist or an user
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FollowerItem {
pub uri: UserId<'static>,
pub name: Option<String>,
pub followers_count: Option<u32>,
pub image_url: Option<String>,
}
/// May be an artist or an user
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub(crate) struct FollowerItemUserlike {
pub uri: UserlikeId<'static>,
pub name: Option<String>,
pub followers_count: Option<u32>,
pub image_url: Option<String>,
}
/// Seektable for AAC tracks
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Seektable {
pub padding_samples: u32,
pub encoder_delay_samples: u32,
pub pssh: String,
pub timescale: u32,
pub init_range: Option<(u32, u32)>,
pub index_range: Option<(u32, u32)>,
pub segments: Vec<(u32, u32)>,
pub offset: usize,
}
/// Information about a track's artists, writers and producers
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackCredits {
pub track_uri: TrackId<'static>,
pub track_title: String,
#[serde(default)]
pub role_credits: Vec<RoleCredits>,
// pub extended_credits: (),
#[serde(default)]
pub source_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoleCredits {
pub role_title: String,
pub artists: Vec<CreditedArtist>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreditedArtist {
pub name: String,
pub uri: Option<ArtistId<'static>>,
pub creator_uri: Option<SongwriterId<'static>>,
pub external_url: Option<String>,
/// Image URL
pub image_uri: Option<String>,
pub subroles: Vec<String>,
pub weight: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistWrap {
pub playlist_v2: PlaylistOption,
}
impl ArtistOption {
pub fn into_option(self) -> Option<ArtistItem> {
match self {
ArtistOption::Artist(artist_item) => Some(artist_item),
ArtistOption::NotFound => None,
}
}
}
impl TrackOption {
pub fn into_option(self) -> Option<TrackItem> {
match self {
TrackOption::Track(track_item) => Some(track_item),
TrackOption::NotFound => None,
}
}
}
impl PlaylistOption {
pub fn into_option(self) -> Option<GqlPlaylistItem> {
match self {
PlaylistOption::Playlist(playlist_item) => Some(playlist_item),
PlaylistOption::NotFound => None,
}
}
}
impl ConcertOption {
pub fn into_option(self) -> Option<ConcertGql> {
match self {
ConcertOption::ConcertV2(concert) => Some(concert),
ConcertOption::NotFound => None,
}
}
}
impl From<GqlWrap<ArtistOption>> for Option<ArtistItem> {
fn from(value: GqlWrap<ArtistOption>) -> Self {
value.data.into_option()
}
}
impl From<GqlWrap<TrackOption>> for Option<TrackItem> {
fn from(value: GqlWrap<TrackOption>) -> Self {
value.data.into_option()
}
}
impl From<GqlWrap<PlaylistOption>> for Option<GqlPlaylistItem> {
fn from(value: GqlWrap<PlaylistOption>) -> Self {
value.data.into_option()
}
}
impl From<GqlWrap<ConcertOption>> for Option<ConcertGql> {
fn from(value: GqlWrap<ConcertOption>) -> Self {
value.data.into_option()
}
}
impl TryFrom<FollowerItemUserlike> for FollowerItem {
type Error = ();
fn try_from(value: FollowerItemUserlike) -> Result<Self, Self::Error> {
if let UserlikeId::User(uri) = value.uri {
Ok(Self {
uri,
name: value.name,
followers_count: value.followers_count,
image_url: value.image_url,
})
} else {
Err(())
}
}
}