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}