1use std::sync::Arc;
5use std::time::Duration;
6
7use serde::Deserialize;
8use serde::de::DeserializeOwned;
9
10use tidal_player_core::{AlbumRef, ArtistRef, Item, Track, TrackId};
11
12use crate::auth::{AuthError, Authenticator};
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
16pub enum MetadataError {
17 #[error("{0} was not found")]
19 NotFound(Item),
20 #[error("malformed {0} response from Tidal")]
22 Malformed(&'static str),
23 #[error(transparent)]
26 Auth(#[from] AuthError),
27}
28
29pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
32
33const SUB_STATUS_NOT_FOUND: u64 = 2001;
36
37const PAGE_SIZE: u64 = 100;
39
40#[derive(Debug, Clone)]
42pub struct MetadataClient {
43 auth: Arc<Authenticator>,
44}
45
46impl MetadataClient {
47 pub fn new(auth: Arc<Authenticator>) -> Self {
48 Self { auth }
49 }
50
51 pub async fn get_track(&self, id: TrackId) -> Result<Track, MetadataError> {
53 let path = format!("tracks/{id}");
54 let dto: TrackDto = self
55 .fetch(&path, &[], "track")
56 .await?
57 .ok_or(MetadataError::NotFound(Item::Track(id)))?;
58 Ok(dto.into())
59 }
60
61 pub async fn get_album_tracks(&self, album: u64) -> Result<Vec<Track>, MetadataError> {
63 let path = format!("albums/{album}/tracks");
64 let pages: Vec<TrackDto> = self
65 .walk(&path, "album", || {
66 MetadataError::NotFound(Item::Album(album))
67 })
68 .await?;
69 Ok(pages.into_iter().map(Track::from).collect())
70 }
71
72 pub async fn get_playlist_tracks(&self, uuid: &str) -> Result<Vec<Track>, MetadataError> {
75 let path = format!("playlists/{uuid}/items");
76 let entries: Vec<PlaylistEntry> = self
77 .walk(&path, "playlist", || {
78 MetadataError::NotFound(Item::Playlist(uuid.to_owned()))
79 })
80 .await?;
81 let mut tracks = Vec::new();
82 for entry in entries.into_iter().filter(|e| e.kind == "track") {
83 let dto = serde_json::from_value::<TrackDto>(entry.item)
84 .map_err(|_| MetadataError::Malformed("playlist"))?;
85 tracks.push(dto.into());
86 }
87 Ok(tracks)
88 }
89
90 pub async fn get_suggestions(&self, seed: TrackId) -> Result<Vec<Track>, MetadataError> {
93 let path = format!("tracks/{seed}/radio");
94 let limit = PAGE_SIZE.to_string();
95 let page: Option<Page<TrackDto>> = self
96 .fetch(&path, &[("limit", limit.as_str())], "suggestions")
97 .await?;
98 Ok(page
99 .map(|p| p.items.into_iter().map(Track::from).collect())
100 .unwrap_or_default())
101 }
102
103 async fn walk<T: DeserializeOwned>(
107 &self,
108 path: &str,
109 what: &'static str,
110 not_found: impl Fn() -> MetadataError,
111 ) -> Result<Vec<T>, MetadataError> {
112 let limit = PAGE_SIZE.to_string();
113 let mut all = Vec::new();
114 let mut offset: u64 = 0;
115 loop {
116 let offset_text = offset.to_string();
117 let query = [("limit", limit.as_str()), ("offset", offset_text.as_str())];
118 let page: Page<T> = self
119 .fetch(path, &query, what)
120 .await?
121 .ok_or_else(¬_found)?;
122 let received = page.items.len() as u64;
123 all.extend(page.items);
124 offset += received;
125 if received == 0 || offset >= page.total {
126 return Ok(all);
127 }
128 }
129 }
130
131 async fn fetch<T: DeserializeOwned>(
134 &self,
135 path: &str,
136 extra: &[(&str, &str)],
137 what: &'static str,
138 ) -> Result<Option<T>, MetadataError> {
139 let (_, country) = self.auth.account().await;
140 let mut query = vec![("countryCode", country.as_str())];
141 query.extend_from_slice(extra);
142 let response = self.auth.get(path, &query, Some(REQUEST_TIMEOUT)).await?;
143 let status = response.status();
144 if status.is_success() {
145 return response
146 .json()
147 .map(Some)
148 .map_err(|_| MetadataError::Malformed(what));
149 }
150 if status.as_u16() == 404 && response.sub_status() == Some(SUB_STATUS_NOT_FOUND) {
151 return Ok(None);
152 }
153 Err(AuthError::Http {
154 status: status.as_u16(),
155 code: None,
156 }
157 .into())
158 }
159}
160
161#[derive(Deserialize)]
162struct Page<T> {
163 #[serde(rename = "totalNumberOfItems")]
164 total: u64,
165 items: Vec<T>,
166}
167
168#[derive(Deserialize)]
169struct PlaylistEntry {
170 #[serde(rename = "type")]
171 kind: String,
172 item: serde_json::Value,
173}
174
175#[derive(Deserialize)]
176pub(crate) struct ArtistDto {
177 pub(crate) id: u64,
178 pub(crate) name: String,
179}
180
181#[derive(Deserialize)]
182struct AlbumDto {
183 id: u64,
184 title: String,
185 #[serde(default)]
187 cover: Option<String>,
188}
189
190impl From<ArtistDto> for ArtistRef {
191 fn from(dto: ArtistDto) -> Self {
192 ArtistRef {
193 id: dto.id,
194 name: dto.name,
195 }
196 }
197}
198
199#[derive(Deserialize)]
201pub(crate) struct TrackDto {
202 id: u64,
203 title: String,
204 version: Option<String>,
206 duration: Option<u64>,
208 #[serde(default, rename = "allowStreaming")]
209 allow_streaming: bool,
210 #[serde(default, rename = "streamReady")]
211 stream_ready: bool,
212 #[serde(default)]
213 artists: Vec<ArtistDto>,
214 artist: Option<ArtistDto>,
215 album: Option<AlbumDto>,
216 #[serde(default, rename = "audioModes")]
219 pub(crate) audio_modes: Vec<String>,
220}
221
222impl From<TrackDto> for Track {
223 fn from(dto: TrackDto) -> Self {
224 let artists: Vec<ArtistRef> = if dto.artists.is_empty() {
225 dto.artist.into_iter().map(ArtistRef::from).collect()
226 } else {
227 dto.artists.into_iter().map(ArtistRef::from).collect()
228 };
229 Track {
230 id: TrackId(dto.id),
231 title: dto.title,
232 version: dto.version,
233 artists,
234 album: dto.album.map(|a| AlbumRef {
235 id: a.id,
236 title: a.title,
237 cover: a.cover,
238 }),
239 duration: dto.duration.map(Duration::from_secs),
240 streamable: dto.allow_streaming && dto.stream_ready,
241 }
242 }
243}