use crate::AudioQuality;
use crate::Error;
use crate::Order;
use crate::OrderDirection;
use crate::TIDAL_API_BASE_URL;
use crate::TidalClient;
use crate::MediaMetadata;
use crate::List;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};
use crate::artist::ArtistSummary;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Track {
pub id: u64,
pub track_number: u32,
#[serde(default = "Default::default")]
pub artists: Vec<ArtistSummary>,
pub album: AlbumSummary,
pub audio_quality: AudioQuality,
pub duration: u32,
pub explicit: bool,
pub isrc: Option<String>,
pub popularity: u32,
pub title: String,
#[serde(rename = "mediaMetadata")]
pub media_metadata: Option<MediaMetadata>,
pub copyright: Option<String>,
pub url: Option<String>,
pub bpm: Option<u32>,
pub upload: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AlbumSummary {
pub id: u64,
pub title: String,
pub cover: Option<String>,
pub release_date: Option<String>,
pub vibrant_color: Option<String>,
pub video_cover: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FavoriteTrack {
pub created: String,
pub item: Track,
}
impl TidalClient {
#[allow(clippy::too_many_arguments)]
pub async fn track_stream(
&self,
track_id: u64,
audio_quality: AudioQuality,
) -> Result<TrackStream, Error> {
let url = format!("{TIDAL_API_BASE_URL}/tracks/{track_id}/urlpostpaywall");
let audio_quality = match audio_quality {
AudioQuality::Low => "LOW",
AudioQuality::High => "HIGH",
AudioQuality::Lossless => "LOSSLESS",
AudioQuality::HiResLossless => "HI_RES_LOSSLESS", };
let params = serde_json::json!({
"audioquality": audio_quality,
"urlusagemode": "STREAM",
"assetpresentation": "FULL"
});
let resp: TrackStream = self.do_request(Method::GET, &url, Some(params), None).await?;
Ok(resp)
}
pub async fn track(
&self,
track_id: u64,
) -> Result<Track, Error> {
let url = format!("{TIDAL_API_BASE_URL}/tracks/{track_id}");
let params = serde_json::json!({
"countryCode": self.get_country_code(),
"locale": self.get_locale(),
"deviceType": self.get_device_type().as_ref(),
});
let resp: Track = self.do_request(Method::GET, &url, Some(params), None).await?;
Ok(resp)
}
pub async fn track_playback_info(
&self,
track_id: u64,
audio_quality: AudioQuality,
) -> Result<TrackPlaybackInfo, Error> {
let url = format!("{TIDAL_API_BASE_URL}/tracks/{track_id}/playbackinfo");
let params = serde_json::json!({
"audioquality": audio_quality.as_ref(),
"playbackmode": "STREAM",
"assetpresentation": "FULL"
});
let resp: TrackPlaybackInfo = self.do_request(Method::GET, &url, Some(params), None).await?;
Ok(resp)
}
#[allow(clippy::too_many_arguments)]
pub async fn track_dash_playback_info(
&self,
track_id: u64,
audio_quality: AudioQuality,
) -> Result<TrackDashPlaybackInfo, Error> {
let url = format!("{TIDAL_API_BASE_URL}/tracks/{track_id}/playbackinfopostpaywall");
let audio_quality = match audio_quality {
AudioQuality::Low => "LOW",
AudioQuality::High => "HIGH",
AudioQuality::Lossless => "LOSSLESS",
AudioQuality::HiResLossless => "HI_RES_LOSSLESS", };
let params = serde_json::json!({
"audioquality": audio_quality,
"playbackmode": "STREAM",
"assetpresentation": "FULL",
"countryCode": self.get_country_code(),
});
let resp: TrackDashPlaybackInfo = self.do_request(Method::GET, &url, Some(params), None).await?;
Ok(resp)
}
pub async fn favorite_tracks(
&self,
offset: Option<u32>,
limit: Option<u32>,
order: Option<Order>,
order_direction: Option<OrderDirection>,
) -> Result<List<FavoriteTrack>, Error> {
let user_id = self.get_user_id().ok_or(Error::UserAuthenticationRequired)?;
let offset = offset.unwrap_or(0);
let limit = limit.unwrap_or(100);
let url = format!("{TIDAL_API_BASE_URL}/users/{user_id}/favorites/tracks");
let params = serde_json::json!({
"offset": offset,
"limit": limit,
"order": order.unwrap_or(Order::Date).as_ref(),
"orderDirection": order_direction.unwrap_or(OrderDirection::Desc).as_ref(),
"countryCode": self.get_country_code(),
"locale": self.get_locale(),
"deviceType": self.get_device_type().as_ref(),
});
let resp: List<FavoriteTrack> = self.do_request(Method::GET, &url, Some(params), None).await?;
Ok(resp)
}
pub async fn add_favorite_track(
&self,
track_id: u64,
) -> Result<(), Error> {
let user_id = self.get_user_id().ok_or(Error::UserAuthenticationRequired)?;
let url = format!("{TIDAL_API_BASE_URL}/users/{user_id}/favorites/tracks");
let params = serde_json::json!({
"trackId": track_id,
"countryCode": self.get_country_code(),
"locale": self.get_locale(),
"deviceType": self.get_device_type().as_ref(),
});
let _: Value = self.do_request(Method::POST, &url, Some(params), None).await?;
Ok(())
}
pub async fn remove_favorite_track(
&self,
track_id: u64,
) -> Result<(), Error> {
let user_id = self.get_user_id().ok_or(Error::UserAuthenticationRequired)?;
let url = format!("{TIDAL_API_BASE_URL}/users/{user_id}/favorites/tracks/{track_id}");
let params = serde_json::json!({
"countryCode": self.get_country_code(),
"locale": self.get_locale(),
"deviceType": self.get_device_type().as_ref(),
});
let _: Value = self.do_request(Method::DELETE, &url, Some(params), None).await?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackStream {
pub asset_presentation: String,
pub audio_mode: String,
pub audio_quality: AudioQuality,
pub codec: String,
pub security_token: Option<String>,
pub security_type: Option<String>,
pub streaming_session_id: Option<String>,
pub track_id: u64,
pub urls: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackPlaybackInfo {
pub album_peak_amplitude: f64,
pub album_replay_gain: f64,
pub asset_presentation: String,
pub audio_mode: String,
pub audio_quality: String,
pub bit_depth: Option<u8>,
pub manifest: String,
pub manifest_hash: String,
pub manifest_mime_type: String,
pub sample_rate: Option<u32>,
pub track_id: u64,
pub track_peak_amplitude: f64,
pub track_replay_gain: f64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrackDashPlaybackInfo {
pub album_peak_amplitude: f64,
pub album_replay_gain: f64,
pub asset_presentation: String,
pub audio_mode: String,
pub audio_quality: AudioQuality,
pub bit_depth: u32,
pub manifest: String,
pub manifest_hash: String,
pub manifest_mime_type: String,
pub sample_rate: u32,
pub track_id: u64,
pub track_peak_amplitude: f64,
pub track_replay_gain: f64,
}
impl TrackDashPlaybackInfo {
pub fn unpack_manifest(&self) -> Result<String, base64::DecodeError> {
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD.decode(self.manifest.as_bytes())?;
Ok(String::from_utf8(decoded).expect("Failed to decode manifest, not UTF-8 XML"))
}
}
impl TrackStream {
pub fn primary_url(&self) -> Option<&str> {
self.urls.get(0).map(|s| s.as_str())
}
pub async fn stream(&self) -> Result<StreamDownload<MemoryStorageProvider>, Error> {
let url: reqwest::Url = match self.primary_url() {
Some(url) => url.parse().expect("Failed to parse stream URL"),
None => return Err(Error::NoPrimaryUrl),
};
let reader =
match StreamDownload::new_http(url, MemoryStorageProvider, Settings::default()).await {
Ok(reader) => reader,
Err(e) => {
return Err(Error::StreamInitializationError(e.to_string()));
}
};
Ok(reader)
}
}