tidalrs/lib.rs
1#![doc = include_str!("../README.md")]
2
3mod album;
4mod artist;
5mod playlist;
6mod search;
7mod track;
8
9pub use album::*;
10pub use artist::*;
11pub use playlist::*;
12pub use search::*;
13pub use track::*;
14
15use arc_swap::ArcSwapOption;
16use async_recursion::async_recursion;
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18use std::fmt::Display;
19use std::sync::{Arc, Mutex};
20use std::time::Duration;
21use strum_macros::{AsRefStr, EnumString};
22use tokio::sync::{Semaphore, SemaphorePermit};
23use tokio::time::sleep;
24
25pub(crate) static TIDAL_AUTH_API_BASE_URL: &str = "https://auth.tidal.com/v1";
26pub(crate) static TIDAL_API_BASE_URL: &str = "https://api.tidal.com/v1";
27const INITIAL_BACKOFF_MILLIS: u64 = 100;
28const DEFAULT_MAX_BACKOFF_MILLIS: u64 = 5_000;
29
30/// Response from the device authorization endpoint containing the information
31/// needed for the user to complete the OAuth2 device flow.
32///
33/// # Example
34///
35/// ```no_run
36/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
37/// # let client = tidalrs::TidalClient::new("client_id".to_string());
38/// let device_auth = client.device_authorization().await?;
39/// println!("Visit: {}", device_auth.url);
40/// println!("Enter code: {}", device_auth.user_code);
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Debug, Serialize, Deserialize, Clone)]
45#[serde(rename_all = "camelCase")]
46pub struct DeviceAuthorizationResponse {
47 /// The URL the user should visit to authorize the application
48 #[serde(rename = "verificationUriComplete")]
49 pub url: String,
50 /// The device code used to complete the authorization flow
51 pub device_code: String,
52 /// How long the device code remains valid (in seconds)
53 pub expires_in: u64,
54 /// The code the user enters on the authorization page
55 pub user_code: String,
56}
57
58/// Represents a Tidal user account with all associated profile information.
59///
60/// This structure contains user data returned during authentication
61/// and can be used to identify the authenticated user.
62#[derive(Debug, Serialize, Deserialize, Clone)]
63#[serde(rename_all = "camelCase")]
64pub struct User {
65 /// Whether the user has accepted the End User License Agreement
66 #[serde(rename = "acceptedEULA")]
67 pub accepted_eula: bool,
68 /// Whether an account link has been created
69 pub account_link_created: bool,
70 /// User's address (if provided)
71 pub address: Option<String>,
72 /// Apple ID associated with the account (if any)
73 pub apple_uid: Option<String>,
74 /// Channel ID associated with the user
75 pub channel_id: u64,
76 /// User's city (if provided)
77 pub city: Option<String>,
78 /// User's country code (e.g., "US", "GB")
79 pub country_code: String,
80 /// Unix timestamp when the account was created
81 pub created: u64,
82 /// User's email address
83 pub email: String,
84 /// Whether the email address has been verified
85 pub email_verified: bool,
86 /// Facebook UID associated with the account (if any)
87 pub facebook_uid: Option<u64>,
88 /// User's first name (if provided)
89 pub first_name: Option<String>,
90 /// User's full name (if provided)
91 pub full_name: Option<String>,
92 /// Google UID associated with the account
93 pub google_uid: Option<String>,
94 /// User's last name (if provided)
95 pub last_name: Option<String>,
96 /// Whether this is a new user account
97 pub new_user: bool,
98 /// User's nickname (if provided)
99 pub nickname: Option<String>,
100 /// Parent ID associated with the user
101 pub parent_id: u64,
102 /// User's phone number (if provided)
103 pub phone_number: Option<String>,
104 /// User's postal code (if provided)
105 pub postalcode: Option<String>,
106 /// Unix timestamp when the account was last updated
107 pub updated: u64,
108 /// User's US state (if provided and in US)
109 pub us_state: Option<String>,
110 /// Unique user ID
111 pub user_id: u64,
112 /// User's username
113 pub username: String,
114}
115
116/// Complete authorization token response from Tidal's OAuth2 endpoint.
117///
118/// This contains all the tokens and user information needed to authenticate
119/// API requests and manage the user session.
120#[derive(Debug, Serialize, Deserialize, Clone)]
121#[serde(rename_all = "camelCase")]
122pub struct AuthzToken {
123 /// Access token for API authentication
124 #[serde(rename = "access_token")]
125 pub access_token: String,
126 /// Name of the client application
127 pub client_name: String,
128 /// Token expiration time in seconds
129 #[serde(rename = "expires_in")]
130 pub expires_in: i64,
131 /// Refresh token for obtaining new access tokens
132 #[serde(rename = "refresh_token")]
133 pub refresh_token: Option<String>,
134 /// OAuth2 scope granted to the application
135 pub scope: String,
136 /// Type of token (typically "Bearer")
137 #[serde(rename = "token_type")]
138 pub token_type: String,
139 /// User information
140 pub user: User,
141 /// User ID (same as user.user_id but as i64)
142 #[serde(rename = "user_id")]
143 pub user_id: i64,
144}
145
146impl AuthzToken {
147 pub fn authz(&self) -> Option<Authz> {
148 if let Some(refresh_token) = self.refresh_token.clone() {
149 Some(Authz {
150 access_token: self.access_token.clone(),
151 refresh_token: refresh_token,
152 user_id: self.user_id as u64,
153 country_code: Some(self.user.country_code.clone()),
154 })
155 } else {
156 None
157 }
158 }
159}
160
161/// Error response from the Tidal API.
162///
163/// This represents errors returned by Tidal's API endpoints and includes
164/// both HTTP status codes and Tidal-specific error information.
165#[derive(Debug, Serialize, Clone)]
166pub struct TidalApiError {
167 /// HTTP status code
168 pub status: u16,
169 /// Tidal-specific sub-status code
170 pub sub_status: u64,
171 /// Human-readable error message
172 pub user_message: String,
173}
174
175impl<'de> Deserialize<'de> for TidalApiError {
176 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177 where
178 D: serde::Deserializer<'de>,
179 {
180 // First deserialize to a generic Value
181 let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?;
182
183 // Extract status (should be consistent)
184 // TODO: Apparently this *isn't* consistent, so we need to handle it better
185 let status = value
186 .get("status")
187 .and_then(|v| v.as_u64())
188 .ok_or_else(|| serde::de::Error::custom("Missing or invalid 'status' field"))?
189 as u16;
190
191 // Extract sub_status - try both snake_case and camelCase
192 let sub_status = value
193 .get("sub_status")
194 .or_else(|| value.get("subStatus"))
195 .and_then(|v| v.as_u64())
196 .ok_or_else(|| {
197 serde::de::Error::custom("Missing or invalid 'sub_status'/'subStatus' field")
198 })?;
199
200 // Extract user_message - try both snake_case and camelCase, default to empty string
201 let user_message = value
202 .get("user_message")
203 .or_else(|| value.get("userMessage"))
204 .and_then(|v| v.as_str())
205 .unwrap_or("")
206 .to_string();
207
208 Ok(TidalApiError {
209 status,
210 sub_status,
211 user_message,
212 })
213 }
214}
215
216impl Display for TidalApiError {
217 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218 write!(
219 f,
220 "Tidal API error: {} {} {}",
221 self.status, self.sub_status, self.user_message
222 )
223 }
224}
225
226/// Errors that can occur when using the TidalRS library.
227///
228/// This enum covers all possible error conditions including network issues,
229/// API errors, authentication problems, and streaming issues.
230#[derive(Debug, thiserror::Error)]
231pub enum Error {
232 /// HTTP request failed (network issues, timeouts, etc.)
233 #[error(transparent)]
234 Http(#[from] reqwest::Error),
235 /// Tidal API returned an error response
236 #[error("Tidal API error: {0}")]
237 TidalApiError(TidalApiError),
238 /// No authorization token available for refresh
239 #[error("No authz token available to refresh client authorization")]
240 NoAuthzToken,
241 /// JSON serialization/deserialization failed
242 #[error(transparent)]
243 SerdeJson(#[from] serde_json::Error),
244 /// No primary streaming URL available for the track
245 #[error("No primary streaming URL available")]
246 NoPrimaryUrl,
247 /// Failed to initialize audio stream
248 #[error("Stream initialization error: {0}")]
249 StreamInitializationError(String),
250 /// No access token available - client needs authentication
251 #[error("No access token available - have you authorized the client?")]
252 NoAccessTokenAvailable,
253 /// Requested audio quality not available for this track
254 #[error("Track at this playback quality not available, try a lower quality")]
255 TrackQualityNotAvailable,
256 /// User authentication required for this operation
257 #[error("User authentication required - please login first")]
258 UserAuthenticationRequired,
259 /// Track not found in the specified playlist
260 #[error("Track {1} not found on playlist {0}")]
261 PlaylistTrackNotFound(String, u64),
262 /// Exponential backoff exceeded the maximum duration while handling rate limits
263 #[error("Hit rate limit backoff ceiling of {0}ms without recovery")]
264 RateLimitBackoffExceeded(u64),
265}
266
267/// Callback function type for handling authorization token refresh events.
268///
269/// This callback is invoked whenever the client automatically refreshes
270/// the access token. Use this to persist updated tokens to storage.
271pub type AuthzCallback = Arc<dyn Fn(Authz) + Send + Sync>;
272
273/// Main client for interacting with the Tidal API.
274///
275/// The `TidalClient` provides an interface for accessing Tidal's
276/// music catalog, managing user data, and streaming audio content. It handles
277/// authentication, automatic token refresh, and provides type-safe methods
278/// for all API operations.
279///
280/// # Example
281///
282/// ```no_run
283/// use tidalrs::{TidalClient, Authz};
284///
285/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
286/// // Create a new client
287/// let mut client = TidalClient::new("your_client_id".to_string());
288///
289/// // Authenticate using device flow
290/// let device_auth = client.device_authorization().await?;
291/// println!("Visit: {}", device_auth.url);
292///
293/// // Complete authentication
294/// let authz_token = client.authorize(&device_auth.device_code, "client_secret").await?;
295///
296/// // Now use the authenticated client
297/// let track = client.track(123456789).await?;
298/// println!("Playing: {}", track.title);
299/// # Ok(())
300/// # }
301/// ```
302///
303/// # Thread Safety
304///
305/// `TidalClient` is designed to be used across multiple threads safely.
306/// All methods are async and the client uses internal synchronization
307/// for token management.
308pub struct TidalClient {
309 pub client: reqwest::Client,
310 client_id: String,
311 authz: ArcSwapOption<Authz>,
312 authz_update_semaphore: Semaphore,
313 country_code: Option<String>,
314 locale: Option<String>,
315 device_type: Option<DeviceType>,
316 on_authz_refresh_callback: Option<AuthzCallback>,
317 backoff: Mutex<Option<u64>>,
318 max_backoff_millis: Option<u64>,
319}
320
321/// Authorization tokens and user information for API access.
322///
323/// This structure contains the authentication data needed to make
324/// authenticated requests to the Tidal API. It can be serialized and stored
325/// persistently to avoid re-authentication.
326///
327/// # Example
328///
329/// ```no_run
330/// use tidalrs::{Authz, TidalClient};
331///
332/// // Create Authz from stored tokens
333/// let authz = Authz::new(
334/// "access_token".to_string(),
335/// "refresh_token".to_string(),
336/// 12345,
337/// Some("US".to_string()),
338/// );
339///
340/// // Create client with existing authentication
341/// let client = TidalClient::new("client_id".to_string())
342/// .with_authz(authz);
343/// ```
344#[derive(Clone, Debug, Serialize, Deserialize)]
345pub struct Authz {
346 /// Access token for API authentication
347 pub access_token: String,
348 /// Refresh token for obtaining new access tokens
349 pub refresh_token: String,
350 /// User ID associated with these tokens
351 pub user_id: u64,
352 /// User's country code (affects content availability)
353 pub country_code: Option<String>,
354}
355
356impl Authz {
357 pub fn new(
358 access_token: String,
359 refresh_token: String,
360 user_id: u64,
361 country_code: Option<String>,
362 ) -> Self {
363 Self {
364 access_token,
365 refresh_token,
366 user_id,
367 country_code,
368 }
369 }
370}
371
372impl TidalClient {
373 /// Create a new TidalClient with the given client ID.
374 ///
375 /// # Arguments
376 ///
377 /// * `client_id` - Your Tidal API client ID
378 ///
379 /// # Example
380 ///
381 /// ```no_run
382 /// use tidalrs::TidalClient;
383 ///
384 /// let client = TidalClient::new("your_client_id".to_string());
385 /// ```
386 pub fn new(client_id: String) -> Self {
387 Self {
388 client: reqwest::Client::new(),
389 client_id,
390 authz: ArcSwapOption::from(None),
391 authz_update_semaphore: Semaphore::new(1),
392 country_code: None,
393 locale: None,
394 device_type: None,
395 on_authz_refresh_callback: None,
396 backoff: Mutex::new(None),
397 max_backoff_millis: None,
398 }
399 }
400
401 /// Set a custom HTTP client using the builder pattern.
402 ///
403 /// This is useful when you need to configure the HTTP client with custom
404 /// settings like timeouts, proxies, or custom headers.
405 ///
406 /// # Arguments
407 ///
408 /// * `client` - Custom reqwest HTTP client
409 ///
410 /// # Example
411 ///
412 /// ```no_run
413 /// use tidalrs::TidalClient;
414 ///
415 /// let custom_client = reqwest::Client::builder()
416 /// .timeout(std::time::Duration::from_secs(30))
417 /// .build()
418 /// .unwrap();
419 ///
420 /// let client = TidalClient::new("client_id".to_string())
421 /// .with_client(custom_client);
422 /// ```
423 pub fn with_client(mut self, client: reqwest::Client) -> Self {
424 self.client = client;
425 self
426 }
427
428 /// Set existing authentication tokens using the builder pattern.
429 ///
430 /// This is useful when you have previously stored authentication tokens
431 /// and want to avoid re-authentication. The client will use these tokens
432 /// for API requests and automatically refresh them when needed.
433 ///
434 /// # Arguments
435 ///
436 /// * `authz` - Existing authorization tokens
437 ///
438 /// # Example
439 ///
440 /// ```no_run
441 /// use tidalrs::{TidalClient, Authz};
442 ///
443 /// let authz = Authz::new(
444 /// "access_token".to_string(),
445 /// "refresh_token".to_string(),
446 /// 12345,
447 /// Some("US".to_string()),
448 /// );
449 /// let client = TidalClient::new("client_id".to_string())
450 /// .with_authz(authz);
451 /// ```
452 pub fn with_authz(mut self, authz: Authz) -> Self {
453 self.authz = ArcSwapOption::from_pointee(authz);
454 self
455 }
456
457 /// Set the locale for API requests using the builder pattern.
458 ///
459 /// This affects the language of returned content and metadata. The locale
460 /// should be in the format "language_COUNTRY" (e.g., "en_US", "en_GB", "de_DE").
461 ///
462 /// # Arguments
463 ///
464 /// * `locale` - The locale string (e.g., "en_US", "fr_FR", "de_DE")
465 ///
466 /// # Example
467 ///
468 /// ```no_run
469 /// use tidalrs::TidalClient;
470 ///
471 /// let client = TidalClient::new("client_id".to_string())
472 /// .with_locale("en_GB".to_string());
473 /// ```
474 pub fn with_locale(mut self, locale: String) -> Self {
475 self.locale = Some(locale);
476 self
477 }
478
479 /// Set the device type for API requests using the builder pattern.
480 ///
481 /// This affects the user agent and may influence content availability
482 /// and API behavior. Different device types may have different access
483 /// to certain features or content.
484 ///
485 /// By default, the device type is set to `DeviceType::Browser`.
486 ///
487 /// # Arguments
488 ///
489 /// * `device_type` - The device type to use for API requests
490 ///
491 /// # Example
492 ///
493 /// ```no_run
494 /// use tidalrs::{TidalClient, DeviceType};
495 ///
496 /// let client = TidalClient::new("client_id".to_string())
497 /// .with_device_type(DeviceType::Browser);
498 /// ```
499 pub fn with_device_type(mut self, device_type: DeviceType) -> Self {
500 self.device_type = Some(device_type);
501 self
502 }
503
504 /// Set the country code for API requests using the builder pattern.
505 ///
506 /// This affects content availability and regional restrictions. The country
507 /// code should be a two-letter ISO country code (e.g., "US", "GB", "DE").
508 /// This setting takes priority over the country code from authentication.
509 ///
510 /// # Arguments
511 ///
512 /// * `country_code` - Two-letter ISO country code (e.g., "US", "GB", "DE")
513 ///
514 /// # Example
515 ///
516 /// ```no_run
517 /// use tidalrs::TidalClient;
518 ///
519 /// let client = TidalClient::new("client_id".to_string())
520 /// .with_country_code("GB".to_string());
521 /// ```
522 pub fn with_country_code(mut self, country_code: String) -> Self {
523 self.country_code = Some(country_code);
524 self
525 }
526
527 /// Set a callback function for authorization token refresh using the builder pattern.
528 ///
529 /// This callback is invoked whenever the client automatically refreshes
530 /// the access token. Use this to persist updated tokens to storage when
531 /// they are automatically refreshed by the client.
532 ///
533 /// # Arguments
534 ///
535 /// * `authz_refresh_callback` - Callback function that receives the new `Authz` when tokens are refreshed
536 ///
537 /// # Example
538 ///
539 /// ```no_run
540 /// use tidalrs::TidalClient;
541 /// use std::sync::Arc;
542 ///
543 /// let client = TidalClient::new("client_id".to_string())
544 /// .with_authz_refresh_callback(|new_authz| {
545 /// println!("Tokens refreshed for user: {}", new_authz.user_id);
546 /// // Save tokens to persistent storage
547 /// todo!();
548 /// });
549 /// ```
550 pub fn with_authz_refresh_callback<F>(mut self, authz_refresh_callback: F) -> Self
551 where
552 F: Fn(Authz) + Send + Sync + 'static,
553 {
554 self.on_authz_refresh_callback = Some(Arc::new(authz_refresh_callback));
555 self
556 }
557
558 /// Set the maximum backoff time in milliseconds for rate limit retries using the builder pattern.
559 ///
560 /// When the client encounters a 429 (Too Many Requests) or 500 (Internal Server Error) response,
561 /// it will retry the request with exponential backoff. This setting controls the maximum
562 /// backoff time before giving up.
563 ///
564 /// Setting this to `0` disables backoff retries entirely - the client will immediately
565 /// return errors for 429 and 500 responses without retrying.
566 ///
567 /// The default value is 5000ms (5 seconds).
568 ///
569 /// # Arguments
570 ///
571 /// * `max_backoff_millis` - Maximum backoff time in milliseconds, or `0` to disable retries
572 ///
573 /// # Example
574 ///
575 /// ```no_run
576 /// use tidalrs::TidalClient;
577 ///
578 /// // Disable backoff retries
579 /// let client = TidalClient::new("client_id".to_string())
580 /// .with_max_backoff_millis(0);
581 ///
582 /// // Set custom max backoff to 10 seconds
583 /// let client = TidalClient::new("client_id".to_string())
584 /// .with_max_backoff_millis(10_000);
585 /// ```
586 pub fn with_max_backoff_millis(mut self, max_backoff_millis: u64) -> Self {
587 self.max_backoff_millis = Some(max_backoff_millis);
588 self
589 }
590
591 /// Get the current country code for API requests.
592 ///
593 /// Returns the explicitly set country code, or falls back to the user's
594 /// country from their authentication, or "US" as a final fallback.
595 pub fn get_country_code(&self) -> String {
596 match &self.country_code {
597 Some(country_code) => country_code.clone(),
598 None => match &self.get_authz() {
599 Some(authz) => authz.country_code.clone().unwrap_or_else(|| "US".into()),
600 None => "US".into(),
601 },
602 }
603 }
604
605 /// Get the current locale for API requests.
606 ///
607 /// Returns the explicitly set locale or "en_US" as default.
608 pub fn get_locale(&self) -> String {
609 self.locale.clone().unwrap_or_else(|| "en_US".into())
610 }
611
612 /// Get the current device type for API requests.
613 ///
614 /// Returns the explicitly set device type or `DeviceType::Browser` as default.
615 pub fn get_device_type(&self) -> DeviceType {
616 self.device_type.unwrap_or_else(|| DeviceType::Browser)
617 }
618
619 /// Get the current user ID if authenticated.
620 ///
621 /// Returns `None` if the client is not authenticated.
622 pub fn get_user_id(&self) -> Option<u64> {
623 self.get_authz().map(|authz| authz.user_id)
624 }
625
626 /// Set the country code for API requests.
627 ///
628 /// This affects content availability and regional restrictions.
629 pub fn set_country_code(&mut self, country_code: String) {
630 self.country_code = Some(country_code);
631 }
632
633 /// Set the locale for API requests.
634 ///
635 /// This affects the language of returned content and metadata.
636 pub fn set_locale(&mut self, locale: String) {
637 self.locale = Some(locale);
638 }
639
640 /// Set the device type for API requests.
641 ///
642 /// This may affect content availability and API behavior.
643 pub fn set_device_type(&mut self, device_type: DeviceType) {
644 self.device_type = Some(device_type);
645 }
646
647 /// Set the maximum backoff time in milliseconds for rate limit retries.
648 ///
649 /// When the client encounters a 429 (Too Many Requests) or 500 (Internal Server Error) response,
650 /// it will retry the request with exponential backoff. This setting controls the maximum
651 /// backoff time before giving up.
652 ///
653 /// Setting this to `0` disables backoff retries entirely - the client will immediately
654 /// return errors for 429 and 500 responses without retrying.
655 ///
656 /// The default value is 5000ms (5 seconds).
657 ///
658 /// # Arguments
659 ///
660 /// * `max_backoff_millis` - Maximum backoff time in milliseconds, or `0` to disable retries
661 pub fn set_max_backoff_millis(&mut self, max_backoff_millis: u64) {
662 self.max_backoff_millis = Some(max_backoff_millis);
663 }
664
665 /// Get the maximum backoff time in milliseconds for rate limit retries.
666 ///
667 /// Returns the configured value or the default (5000ms).
668 pub fn get_max_backoff_millis(&self) -> u64 {
669 self.max_backoff_millis
670 .unwrap_or(DEFAULT_MAX_BACKOFF_MILLIS)
671 }
672
673 /// Set a callback function to be called when authorization tokens are refreshed.
674 ///
675 /// This is useful for persisting updated tokens to storage when they are
676 /// automatically refreshed by the client.
677 ///
678 /// # Example
679 ///
680 /// ```no_run
681 /// use tidalrs::TidalClient;
682 /// use std::sync::Arc;
683 ///
684 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
685 /// let client = TidalClient::new("client_id".to_string())
686 /// .with_authz_refresh_callback(Arc::new(|new_authz| {
687 /// println!("Tokens refreshed for user: {}", new_authz.user_id);
688 /// // Save tokens to persistent storage
689 /// }));
690 /// # Ok(())
691 /// # }
692 /// ```
693 pub fn on_authz_refresh<F>(&mut self, f: F)
694 where
695 F: Fn(Authz) + Send + Sync + 'static,
696 {
697 self.on_authz_refresh_callback = Some(Arc::new(f));
698 }
699
700 /// Get the current authorization tokens.
701 ///
702 /// Returns `None` if the client is not authenticated. This is useful for
703 /// persisting tokens when shutting down the client.
704 ///
705 /// # Example
706 ///
707 /// ```no_run
708 /// use tidalrs::TidalClient;
709 ///
710 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
711 /// # let client = TidalClient::new("client_id".to_string());
712 /// if let Some(authz) = client.get_authz() {
713 /// // Save tokens for next session
714 /// println!("User ID: {}", authz.user_id);
715 /// }
716 /// # Ok(())
717 /// # }
718 /// ```
719 pub fn get_authz(&self) -> Option<Arc<Authz>> {
720 self.authz.load_full()
721 }
722
723 #[async_recursion]
724 async fn refresh_authz(&self) -> Result<(), Error> {
725 // Try to become the single refresher
726 let permit: Option<SemaphorePermit> = match self.authz_update_semaphore.try_acquire() {
727 Ok(p) => Some(p),
728 Err(_) => None,
729 };
730
731 match permit {
732 // We're the single refresher, fetch the new authz and update the client
733 Some(permit) => {
734 let url = format!("{TIDAL_AUTH_API_BASE_URL}/oauth2/token");
735
736 let authz = self.get_authz().ok_or(Error::NoAuthzToken)?;
737
738 let params = serde_json::json!({
739 "client_id": &self.client_id,
740 "refresh_token": authz.refresh_token,
741 "grant_type": "refresh_token",
742 "scope": "r_usr w_usr",
743 });
744
745 let resp: AuthzToken = self
746 .do_request(reqwest::Method::POST, &url, Some(params), None)
747 .await?;
748
749 let new_authz = Authz {
750 access_token: resp.access_token,
751 refresh_token: resp
752 .refresh_token
753 .unwrap_or_else(|| authz.refresh_token.clone()),
754 user_id: resp.user.user_id,
755 country_code: match &authz.country_code {
756 Some(country_code) => Some(country_code.clone()),
757 None => Some(resp.user.country_code.clone()),
758 },
759 };
760
761 // Single, quick swap visible to all readers
762 self.authz.store(Some(Arc::new(new_authz.clone())));
763
764 drop(permit);
765
766 // invoke callback if set
767 if let Some(cb) = &self.on_authz_refresh_callback {
768 cb(new_authz);
769 }
770
771 Ok(())
772 }
773 None => {
774 // Someone else is refreshing—await completion cooperatively
775 // Acquire then drop to wait for the in-flight refresh to finish.
776 let _ = self.authz_update_semaphore.acquire().await;
777 Ok(())
778 }
779 }
780 }
781
782 // Do a GET or DELETE request to the given URL.
783 #[async_recursion]
784 pub(crate) async fn do_request<T: DeserializeOwned>(
785 &self,
786 method: reqwest::Method,
787 url: &str,
788 params: Option<serde_json::Value>,
789 etag: Option<&str>,
790 ) -> Result<T, Error> {
791 self.await_rate_limit_backoff().await;
792
793 let mut req = match method {
794 reqwest::Method::GET => self.client.get(url),
795 reqwest::Method::DELETE => self.client.delete(url),
796 reqwest::Method::POST => self.client.post(url),
797 _ => panic!("Invalid method: {}", method),
798 };
799
800 if let Some(etag) = etag {
801 req = req.header(reqwest::header::IF_NONE_MATCH, etag);
802 }
803
804 if let Some(authz) = self.get_authz() {
805 req = req.header(
806 reqwest::header::AUTHORIZATION,
807 &format!("Bearer {}", authz.access_token),
808 );
809 }
810
811 req = req.header(reqwest::header::USER_AGENT, "Mozilla/5.0 (Linux; Android 12; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/91.0.4472.114 Safari/537.36");
812
813 if let Some(params) = params.as_ref() {
814 match method {
815 reqwest::Method::POST => req = req.form(params),
816 reqwest::Method::GET => req = req.query(params),
817 reqwest::Method::DELETE => req = req.query(params),
818 _ => panic!("Invalid method for params: {}", method),
819 }
820 }
821
822 let resp = req.send().await?;
823
824 let etag: Option<String> = resp.headers().get("ETag").map(|etag| {
825 let etag = etag.to_str().expect("Invalid ETag header").to_string();
826
827 match serde_json::from_str::<String>(&etag) {
828 Ok(etag) => etag,
829 Err(_) => etag,
830 }
831 });
832
833 let status = resp.status();
834 let body = resp.bytes().await?;
835
836 // Parse it into a value
837 let mut value: serde_json::Value = if body.is_empty() {
838 serde_json::Value::Null
839 } else {
840 match serde_json::from_slice(&body) {
841 Ok(value) => value,
842 Err(e) => {
843 let error_message = String::from_utf8_lossy(&body);
844 if log::log_enabled!(log::Level::Warn) {
845 log::warn!("Requested URL: {}", url);
846 log::warn!("JSON deserialization error: {}", e);
847 log::warn!("Response: {}", error_message);
848 }
849 return Err(Error::TidalApiError(TidalApiError {
850 status: status.as_u16(),
851 sub_status: 0,
852 user_message: error_message.to_string(),
853 }));
854 }
855 }
856 };
857
858 log::trace!(
859 "Response from TIDAL: {}",
860 serde_json::to_string_pretty(&value).unwrap()
861 );
862
863 if status.is_success() {
864 self.reset_rate_limit_backoff();
865
866 // If we have an etag, add it to the response, if the value doesn't already exist
867 if let Some(etag) = etag {
868 if value.get("etag").is_none() {
869 value["etag"] = serde_json::Value::String(etag);
870 }
871 }
872
873 let resp: T = match serde_json::from_value(value.clone()) {
874 Ok(t) => t,
875 Err(e) => {
876 if log::log_enabled!(log::Level::Warn) {
877 let problem_value_pretty = serde_json::to_string_pretty(&value).unwrap();
878 log::warn!("Requested URL: {}", url);
879 log::warn!("JSON deserialization error: {}", e);
880 log::warn!("Response: {}", problem_value_pretty);
881 }
882 return Err(Error::TidalApiError(TidalApiError {
883 status: status.as_u16(),
884 sub_status: 0,
885 user_message: e.to_string(),
886 }));
887 }
888 };
889
890 Ok(resp)
891 } else {
892 if status.as_u16() == 429 || status.as_u16() == 500 {
893 // Skip retry if backoff is disabled (max_backoff_millis == 0)
894 if self.get_max_backoff_millis() == 0 {
895 self.reset_rate_limit_backoff();
896 } else {
897 // Increase backoff and retry
898 // The backoff wait will happen at the start of do_request
899 self.increase_rate_limit_backoff()?;
900 return self.do_request(method, url, params, etag.as_deref()).await;
901 }
902 } else {
903 self.reset_rate_limit_backoff();
904 }
905
906 let tidal_err = match serde_json::from_value::<TidalApiError>(value.clone()) {
907 Ok(e) => e,
908 Err(e) => {
909 if log::log_enabled!(log::Level::Warn) {
910 let problem_value_pretty = serde_json::to_string_pretty(&value).unwrap();
911 log::warn!("Requested URL: {}", url);
912 log::warn!("JSON deserialization error of TidalApiError: {}", e);
913 log::warn!("Response: {}", problem_value_pretty);
914 }
915 return Err(Error::TidalApiError(TidalApiError {
916 status: status.as_u16(),
917 sub_status: 0,
918 user_message: e.to_string(),
919 }));
920 }
921 };
922
923 // If it's 401, we need to refresh the authz and try again
924 if status.as_u16() == 401 && tidal_err.sub_status == 11003 {
925 // Expired token, safe to refresh
926 self.refresh_authz().await?;
927 return self.do_request(method, url, params, etag.as_deref()).await;
928 }
929
930 if log::log_enabled!(log::Level::Warn) {
931 let pretty_err = serde_json::to_string_pretty(&tidal_err).unwrap();
932 log::warn!("Requested URL: {}", url);
933 log::warn!("TIDAL API Error: {}", pretty_err);
934 }
935
936 Err(Error::TidalApiError(tidal_err))
937 }
938 }
939
940 async fn await_rate_limit_backoff(&self) {
941 // Skip backoff if disabled
942 if self.get_max_backoff_millis() == 0 {
943 return;
944 }
945
946 let delay = {
947 let guard = self
948 .backoff
949 .lock()
950 .unwrap_or_else(|poisoned| poisoned.into_inner());
951 *guard
952 };
953
954 if let Some(ms) = delay {
955 if ms > 0 {
956 sleep(Duration::from_millis(ms)).await;
957 }
958 }
959 }
960
961 fn increase_rate_limit_backoff(&self) -> Result<(), Error> {
962 let max_backoff = self.get_max_backoff_millis();
963
964 // Skip if backoff is disabled
965 if max_backoff == 0 {
966 return Ok(());
967 }
968
969 let mut guard = self
970 .backoff
971 .lock()
972 .unwrap_or_else(|poisoned| poisoned.into_inner());
973 let next = match *guard {
974 Some(current) => current.saturating_mul(2),
975 None => INITIAL_BACKOFF_MILLIS,
976 };
977
978 if next >= max_backoff {
979 *guard = Some(max_backoff);
980 return Err(Error::RateLimitBackoffExceeded(max_backoff));
981 }
982
983 *guard = Some(next);
984 Ok(())
985 }
986
987 fn reset_rate_limit_backoff(&self) {
988 let mut guard = self
989 .backoff
990 .lock()
991 .unwrap_or_else(|poisoned| poisoned.into_inner());
992 if guard.is_some() {
993 *guard = None;
994 }
995 }
996
997 /// Start the OAuth2 device authorization flow.
998 ///
999 /// This initiates the device flow authentication process. The user must
1000 /// visit the returned URL and enter the user code to complete authentication.
1001 ///
1002 /// # Returns
1003 ///
1004 /// A `DeviceAuthorizationResponse` containing the URL to visit and the
1005 /// user code to enter.
1006 ///
1007 /// # Example
1008 ///
1009 /// ```no_run
1010 /// use tidalrs::TidalClient;
1011 ///
1012 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1013 /// # let client = TidalClient::new("client_id".to_string());
1014 /// let device_auth = client.device_authorization().await?;
1015 /// println!("Visit: {}", device_auth.url);
1016 /// println!("Enter code: {}", device_auth.user_code);
1017 /// # Ok(())
1018 /// # }
1019 /// ```
1020 pub async fn device_authorization(&self) -> Result<DeviceAuthorizationResponse, Error> {
1021 let url = format!("{TIDAL_AUTH_API_BASE_URL}/oauth2/device_authorization");
1022
1023 let params = serde_json::json!({
1024 "client_id": &self.client_id,
1025 "scope": "r_usr w_usr w_sub",
1026 });
1027
1028 let mut resp: DeviceAuthorizationResponse = self
1029 .do_request(reqwest::Method::POST, &url, Some(params), None)
1030 .await?;
1031
1032 resp.url = format!("https://{url}", url = resp.url);
1033
1034 Ok(resp)
1035 }
1036
1037 /// Complete the OAuth2 device authorization flow.
1038 ///
1039 /// Call this method after the user has visited the authorization URL and
1040 /// entered the user code. This completes the authentication process and
1041 /// stores the tokens in the client.
1042 ///
1043 /// # Arguments
1044 ///
1045 /// * `device_code` - The device code from `device_authorization()`
1046 /// * `client_secret` - Your Tidal API client secret
1047 ///
1048 /// # Returns
1049 ///
1050 /// An `AuthzToken` containing all user and token information.
1051 ///
1052 /// # Example
1053 ///
1054 /// ```no_run
1055 /// use tidalrs::TidalClient;
1056 ///
1057 /// let mut client = TidalClient::new("client_id".to_string());
1058 /// let device_code = "device_code";
1059 /// let client_secret = "client_secret";
1060 /// let authz_token = client.authorize(device_code, client_secret).await?;
1061 /// println!("Authenticated as: {}", authz_token.user.username);
1062 ///
1063 /// // Get the authz token to store in persistent storage
1064 /// let authz = authz_token.authz().unwrap();
1065 /// std::fs::write("authz.json", serde_json::to_string(&authz).unwrap()).unwrap();
1066 /// ```
1067 pub async fn authorize(
1068 &self,
1069 device_code: &str,
1070 client_secret: &str,
1071 ) -> Result<AuthzToken, Error> {
1072 let url = format!("{TIDAL_AUTH_API_BASE_URL}/oauth2/token");
1073
1074 let params = serde_json::json!({
1075 "client_id": &self.client_id,
1076 "client_secret": client_secret,
1077 "device_code": &device_code,
1078 "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
1079 "scope": "r_usr w_usr w_sub",
1080 });
1081
1082 let resp: AuthzToken = self
1083 .do_request(reqwest::Method::POST, &url, Some(params), None)
1084 .await?;
1085
1086 let authz = Authz {
1087 access_token: resp.access_token.clone(),
1088 refresh_token: resp
1089 .refresh_token
1090 .clone()
1091 .expect("No refresh token received from Tidal after authorization"),
1092 user_id: resp.user.user_id,
1093 country_code: match &self.country_code {
1094 Some(country_code) => Some(country_code.clone()),
1095 None => Some(resp.user.country_code.clone()),
1096 },
1097 };
1098
1099 self.authz.store(Some(Arc::new(authz)));
1100
1101 Ok(resp)
1102 }
1103}
1104
1105/// Device type for API requests.
1106///
1107/// This affects the user agent and may influence content availability
1108/// and API behavior.
1109#[derive(
1110 Debug, Serialize, Deserialize, Default, EnumString, AsRefStr, PartialEq, Eq, Clone, Copy,
1111)]
1112#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1113#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1114pub enum DeviceType {
1115 /// Browser-based client
1116 #[default]
1117 Browser,
1118}
1119
1120/// Audio quality levels available for streaming.
1121///
1122/// Higher quality levels may require a Tidal HiFi subscription.
1123/// The actual quality available depends on the user's subscription
1124/// and the track's availability.
1125///
1126/// # Example
1127///
1128/// ```no_run
1129/// use tidalrs::{AudioQuality, TidalClient};
1130///
1131/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1132/// # let client = TidalClient::new("client_id".to_string());
1133/// let track_id = 123456789;
1134/// let stream = client.track_stream(track_id, AudioQuality::Lossless).await?;
1135/// # Ok(())
1136/// # }
1137/// ```
1138#[derive(Debug, Serialize, Deserialize, EnumString, AsRefStr, PartialEq, Eq, Clone, Copy)]
1139#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1140#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1141pub enum AudioQuality {
1142 /// Low quality (typically 96 kbps AAC)
1143 Low,
1144 /// High quality (typically 320 kbps AAC)
1145 High,
1146 /// Lossless quality (FLAC, typically 44.1 kHz / 16-bit)
1147 Lossless,
1148 /// Hi-Res Lossless quality (FLAC, up to 192 kHz / 24-bit)
1149 HiResLossless,
1150}
1151
1152/// Sort order for listing operations.
1153#[derive(Debug, Serialize, Deserialize, EnumString, AsRefStr, PartialEq, Eq, Clone, Copy)]
1154#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1155#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1156pub enum Order {
1157 /// Sort by date
1158 Date,
1159}
1160
1161/// Direction for sorting operations.
1162#[derive(Debug, Serialize, Deserialize, EnumString, AsRefStr, PartialEq, Eq, Clone, Copy)]
1163#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1164#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
1165pub enum OrderDirection {
1166 /// Ascending order
1167 Asc,
1168 /// Descending order
1169 Desc,
1170}
1171
1172/// Media metadata associated with tracks and albums.
1173#[derive(Debug, Clone, Serialize, Deserialize)]
1174pub struct MediaMetadata {
1175 /// Tags associated with the media
1176 #[serde(default)]
1177 pub tags: Vec<String>,
1178}
1179
1180/// Types of resources available in the Tidal API.
1181///
1182/// Used for search filtering and resource identification.
1183#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
1184#[serde(rename_all = "UPPERCASE")]
1185pub enum ResourceType {
1186 /// Artist resource
1187 Artist,
1188 /// Album resource
1189 Album,
1190 /// Track resource
1191 Track,
1192 /// Video resource
1193 Video,
1194 /// Playlist resource
1195 Playlist,
1196 /// User profile resource
1197 UserProfile,
1198}
1199
1200impl ResourceType {
1201 pub fn as_str(&self) -> &str {
1202 match self {
1203 ResourceType::Artist => "ARTIST",
1204 ResourceType::Album => "ALBUM",
1205 ResourceType::Track => "TRACK",
1206 ResourceType::Video => "VIDEO",
1207 ResourceType::Playlist => "PLAYLIST",
1208 ResourceType::UserProfile => "USER_PROFILE",
1209 }
1210 }
1211}
1212
1213impl std::str::FromStr for ResourceType {
1214 type Err = ();
1215
1216 fn from_str(s: &str) -> Result<Self, Self::Err> {
1217 match s {
1218 "ARTIST" => Ok(ResourceType::Artist),
1219 "ARTISTS" => Ok(ResourceType::Artist),
1220 "ALBUM" => Ok(ResourceType::Album),
1221 "ALBUMS" => Ok(ResourceType::Album),
1222 "TRACK" => Ok(ResourceType::Track),
1223 "TRACKS" => Ok(ResourceType::Track),
1224 "VIDEO" => Ok(ResourceType::Video),
1225 "VIDEOS" => Ok(ResourceType::Video),
1226 "PLAYLIST" => Ok(ResourceType::Playlist),
1227 "PLAYLISTS" => Ok(ResourceType::Playlist),
1228 "USER_PROFILE" => Ok(ResourceType::UserProfile),
1229 "USER_PROFILES" => Ok(ResourceType::UserProfile),
1230 _ => Err(()),
1231 }
1232 }
1233}
1234
1235impl From<String> for ResourceType {
1236 fn from(s: String) -> Self {
1237 s.parse().unwrap()
1238 }
1239}
1240
1241impl From<&str> for ResourceType {
1242 fn from(s: &str) -> Self {
1243 s.parse().unwrap()
1244 }
1245}
1246
1247/// A unified resource type that can represent any Tidal content.
1248///
1249/// This enum allows handling different types of resources in a type-safe way,
1250/// commonly used in search results and mixed content lists.
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1252#[serde(tag = "type", content = "value", rename_all = "SCREAMING_SNAKE_CASE")]
1253pub enum Resource {
1254 /// Artist resource
1255 Artists(Artist),
1256 /// Album resource
1257 Albums(Album),
1258 /// Track resource
1259 Tracks(Track),
1260 /// Playlist resource
1261 Playlists(Playlist),
1262
1263 // TODO: Add proper support for videos and user profiles
1264 /// Video resource (currently as raw JSON)
1265 Videos(serde_json::Value),
1266 /// User profile resource (currently as raw JSON)
1267 UserProfiles(serde_json::Value),
1268}
1269
1270impl Resource {
1271 pub fn id(&self) -> String {
1272 match self {
1273 Resource::Artists(artist) => artist.id.to_string(),
1274 Resource::Albums(album) => album.id.to_string(),
1275 Resource::Tracks(track) => track.id.to_string(),
1276 Resource::Playlists(playlist) => playlist.uuid.to_string(),
1277 Resource::Videos(video) => video
1278 .get("id")
1279 .unwrap_or(&serde_json::Value::Null)
1280 .to_string(),
1281 Resource::UserProfiles(user_profile) => user_profile
1282 .get("id")
1283 .unwrap_or(&serde_json::Value::Null)
1284 .to_string(),
1285 }
1286 }
1287}
1288
1289/// A paginated list response from the Tidal API.
1290///
1291/// This generic structure is used for all paginated endpoints and provides
1292/// information about the current page and total available items.
1293///
1294/// # Example
1295///
1296/// ```no_run
1297/// use tidalrs::{TidalClient, List};
1298///
1299/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1300/// # let client = TidalClient::new("client_id".to_string());
1301/// let tracks: List<tidalrs::Track> = client.album_tracks(12345, Some(0), Some(50)).await?;
1302///
1303/// println!("Showing {} of {} tracks", tracks.items.len(), tracks.total);
1304/// for track in tracks.items {
1305/// println!("Track: {}", track.title);
1306/// }
1307/// # Ok(())
1308/// # }
1309/// ```
1310#[derive(Debug, Clone, Serialize, Deserialize)]
1311pub struct List<T> {
1312 /// Items in the current page
1313 pub items: Vec<T>,
1314 /// Offset of the current page
1315 pub offset: usize,
1316 /// Maximum number of items per page
1317 pub limit: usize,
1318 /// Total number of items available
1319 #[serde(rename = "totalNumberOfItems")]
1320 pub total: usize,
1321
1322 /// ETag for optimistic concurrency control (used in playlist modifications)
1323 #[serde(skip_serializing_if = "Option::is_none")]
1324 #[serde(default)]
1325 pub etag: Option<String>,
1326}
1327
1328impl<T> List<T> {
1329 pub fn is_empty(&self) -> bool {
1330 self.total == 0
1331 }
1332
1333 // The number of items left to fetch
1334 pub fn num_left(&self) -> usize {
1335 let current_batch_size = self.items.len();
1336 self.total - self.offset - current_batch_size
1337 }
1338}
1339
1340impl<T> Default for List<T> {
1341 fn default() -> Self {
1342 Self {
1343 items: Vec::new(),
1344 offset: 0,
1345 limit: 0,
1346 total: 0,
1347 etag: None,
1348 }
1349 }
1350}
1351
1352// Utility function to deserialize a null value as a default value
1353pub(crate) fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
1354where
1355 D: serde::Deserializer<'de>,
1356 T: Default + serde::Deserialize<'de>,
1357{
1358 Option::deserialize(deserializer).map(|opt| opt.unwrap_or_default())
1359}