Skip to main content

lavende_core/sources/
tidal.rs

1pub mod client {
2    use super::{
3        error::{TidalError, TidalResult},
4        token::TidalTokenTracker,
5    };
6    use serde_json::Value;
7    use std::sync::Arc;
8    const API_BASE: &str = "https://api.tidal.com/v1";
9    pub struct TidalClient {
10        pub inner: Arc<reqwest::Client>,
11        pub token_tracker: Arc<TidalTokenTracker>,
12        pub country_code: String,
13        pub quality: String,
14    }
15    impl TidalClient {
16        pub fn new(
17            inner: Arc<reqwest::Client>,
18            token_tracker: Arc<TidalTokenTracker>,
19            country_code: String,
20            quality: String,
21        ) -> Self {
22            Self {
23                inner,
24                token_tracker,
25                country_code,
26                quality,
27            }
28        }
29        pub fn base_request(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
30            builder
31                .header(
32                    reqwest::header::USER_AGENT,
33                    "TIDAL/3704 CFNetwork/1220.1 Darwin/20.3.0",
34                )
35                .header("Accept-Language", "en-US")
36        }
37        pub async fn get_json(&self, path: &str) -> TidalResult<Value> {
38            let url = if path.starts_with("http") {
39                path.to_owned()
40            } else {
41                format!("{API_BASE}{path}")
42            };
43            let url = if !url.contains("countryCode=") {
44                if url.contains('?') {
45                    format!("{url}&countryCode={}", self.country_code)
46                } else {
47                    format!("{url}?countryCode={}", self.country_code)
48                }
49            } else {
50                url
51            };
52            let mut req = self.base_request(self.inner.get(&url));
53            if let Some(t) = self.token_tracker.get_scraper_token().await {
54                req = req.header("x-tidal-token", t);
55            } else if let Some(t) = self.token_tracker.get_oauth_token().await {
56                req = req.header("Authorization", format!("Bearer {t}"));
57            } else {
58                return Err(TidalError::NoToken);
59            }
60            let resp = req.send().await?;
61            if !resp.status().is_success() {
62                let status = resp.status();
63                let body = resp.text().await.unwrap_or_default();
64                return Err(TidalError::ApiError { status, body });
65            }
66            Ok(resp.json().await?)
67        }
68    }
69}
70pub mod error {
71    use thiserror::Error;
72    #[derive(Debug, Error)]
73    pub enum TidalError {
74        #[error("API request failed: {0}")]
75        Request(#[from] reqwest::Error),
76        #[error("API returned {status}: {body}")]
77        ApiError {
78            status: reqwest::StatusCode,
79            body: String,
80        },
81        #[error("Failed to parse response: {0}")]
82        Parse(#[from] serde_json::Error),
83        #[error("Base64 decode failed: {0}")]
84        Base64(#[from] base64::DecodeError),
85        #[error("Authentication failed: {0}")]
86        Auth(String),
87        #[error("No token available")]
88        NoToken,
89        #[error("No stream URL in manifest")]
90        NoStreamUrl,
91        #[error("Other error: {0}")]
92        Other(String),
93    }
94    pub type TidalResult<T> = Result<T, TidalError>;
95}
96pub mod manager {
97    use super::{
98        client::TidalClient,
99        model::{Manifest, PlaybackInfo},
100        oauth::TidalOAuth,
101        token::TidalTokenTracker,
102        track::TidalTrack,
103    };
104    use crate::{
105        common::types::AudioFormat,
106        protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo, Track, TrackInfo},
107        sources::{SourcePlugin, playable_track::BoxedTrack},
108    };
109    use async_trait::async_trait;
110    use base64::{Engine as _, engine::general_purpose};
111    use regex::Regex;
112    use serde_json::Value;
113    use std::sync::{Arc, OnceLock};
114    use tracing::{debug, warn};
115    fn url_regex() -> &'static Regex {
116        static RE: OnceLock<Regex> = OnceLock::new();
117        RE.get_or_init(|| {
118        Regex::new(r"https?://(?:(?:listen|www)\.)?tidal\.com/(?:browse/)?(album|track|playlist|mix|artist)/([a-zA-Z0-9\-]+)(?:/.*)?(?:\?.*)?").unwrap()
119    })
120    }
121    pub struct TidalSource {
122        pub client: Arc<TidalClient>,
123        playlist_load_limit: usize,
124        album_load_limit: usize,
125        artist_load_limit: usize,
126    }
127    impl TidalSource {
128        pub fn new(
129            config: Option<crate::config::TidalConfig>,
130            http_client: Arc<reqwest::Client>,
131        ) -> Result<Self, String> {
132            let (country, quality, p_limit, a_limit, art_limit, refresh_token, get_oauth_token) =
133                if let Some(c) = config {
134                    (
135                        c.country_code,
136                        c.quality,
137                        c.playlist_load_limit,
138                        c.album_load_limit,
139                        c.artist_load_limit,
140                        c.refresh_token,
141                        c.get_oauth_token,
142                    )
143                } else {
144                    (
145                        "US".to_string(),
146                        crate::config::sources::default_tidal_quality(),
147                        0,
148                        0,
149                        0,
150                        None,
151                        false,
152                    )
153                };
154            let oauth = Arc::new(TidalOAuth::new(refresh_token));
155            if get_oauth_token {
156                let oauth_clone = oauth.clone();
157                tokio::spawn(async move {
158                    oauth_clone.initialize_access_token().await;
159                });
160            }
161            let token_tracker = Arc::new(TidalTokenTracker::new(http_client.clone(), oauth));
162            token_tracker.clone().init();
163            let client = Arc::new(TidalClient::new(
164                http_client,
165                token_tracker,
166                country,
167                quality,
168            ));
169            Ok(Self {
170                client,
171                playlist_load_limit: p_limit,
172                album_load_limit: a_limit,
173                artist_load_limit: art_limit,
174            })
175        }
176        fn parse_track(&self, item: &Value) -> Option<TrackInfo> {
177            let id = item.get("id")?.as_u64()?.to_string();
178            let title = item
179                .get("title")
180                .and_then(|v| v.as_str())
181                .unwrap_or("Unknown Title")
182                .to_string();
183            let artists = item
184                .get("artists")
185                .and_then(|v| v.as_array())
186                .map(|a| {
187                    a.iter()
188                        .filter_map(|x| x.get("name").and_then(|n| n.as_str()))
189                        .collect::<Vec<_>>()
190                        .join(", ")
191                })
192                .unwrap_or_else(|| "Unknown Artist".to_owned());
193            let length = item.get("duration").and_then(|v| v.as_u64()).unwrap_or(0) * 1000;
194            let isrc = item
195                .get("isrc")
196                .and_then(|v| v.as_str())
197                .filter(|s| !s.is_empty())
198                .map(|s| s.to_owned());
199            let artwork_url = item
200                .get("album")
201                .and_then(|a| a.get("cover"))
202                .and_then(|v| v.as_str())
203                .filter(|s| !s.is_empty())
204                .map(|s| {
205                    format!(
206                        "https://resources.tidal.com/images/{}/1280x1280.jpg",
207                        s.replace("-", "/")
208                    )
209                });
210            let url = item
211                .get("url")
212                .and_then(|v| v.as_str())
213                .filter(|s| !s.is_empty())
214                .map(|s| s.replace("http://", "https://"));
215            Some(TrackInfo {
216                title,
217                author: artists,
218                length,
219                identifier: id,
220                is_stream: false,
221                uri: url,
222                artwork_url,
223                isrc,
224                source_name: "tidal".to_owned(),
225                is_seekable: true,
226                position: 0,
227            })
228        }
229        async fn get_track_data(&self, id: &str) -> LoadResult {
230            match self.client.get_json(&format!("/tracks/{id}")).await {
231                Ok(data) => self
232                    .parse_track(&data)
233                    .map(|i| LoadResult::Track(Track::new(i)))
234                    .unwrap_or(LoadResult::Empty {}),
235                Err(_) => LoadResult::Empty {},
236            }
237        }
238        async fn get_album_or_playlist(&self, id: &str, type_str: &str) -> LoadResult {
239            let info_data = match self.client.get_json(&format!("/{type_str}s/{id}")).await {
240                Ok(d) => d,
241                Err(_) => return LoadResult::Empty {},
242            };
243            let title = info_data
244                .get("title")
245                .and_then(|v| v.as_str())
246                .unwrap_or("Unknown")
247                .to_owned();
248            let limit = (if type_str == "album" {
249                self.album_load_limit
250            } else {
251                self.playlist_load_limit
252            })
253            .clamp(1, 100);
254            let tracks_data = match self
255                .client
256                .get_json(&format!("/{type_str}s/{id}/tracks?limit={limit}"))
257                .await
258            {
259                Ok(d) => d,
260                Err(_) => return LoadResult::Empty {},
261            };
262            let mut tracks = Vec::new();
263            if let Some(items) = tracks_data.get("items").and_then(|v| v.as_array()) {
264                for item in items {
265                    let track_obj = item.get("item").unwrap_or(item);
266                    if let Some(info) = self.parse_track(track_obj) {
267                        tracks.push(Track::new(info));
268                    }
269                }
270            }
271            if tracks.is_empty() {
272                return LoadResult::Empty {};
273            }
274            LoadResult::Playlist(PlaylistData {
275                info: PlaylistInfo {
276                    name: title,
277                    selected_track: -1,
278                },
279                plugin_info: serde_json::json!({
280                    "type": type_str,
281                    "url": format!("https://tidal.com/browse/{type_str}/{id}"),
282                    "totalTracks": info_data.get("numberOfTracks").or_else(|| info_data.get("numberOfSongs")).and_then(|v| v.as_u64()).unwrap_or(tracks.len() as u64)
283                }),
284                tracks,
285            })
286        }
287        async fn get_mix(&self, id: &str, name_override: Option<String>) -> LoadResult {
288            let data = match self
289                .client
290                .get_json(&format!("/mixes/{id}/items?limit=100"))
291                .await
292            {
293                Ok(d) => d,
294                Err(_) => return LoadResult::Empty {},
295            };
296            let mut tracks = Vec::new();
297            if let Some(items) = data.get("items").and_then(|v| v.as_array()) {
298                for item in items {
299                    let track_obj = item.get("item").unwrap_or(item);
300                    if let Some(info) = self.parse_track(track_obj) {
301                        tracks.push(Track::new(info));
302                    }
303                }
304            }
305            if tracks.is_empty() {
306                return LoadResult::Empty {};
307            }
308            LoadResult::Playlist(PlaylistData {
309                info: PlaylistInfo {
310                    name: name_override.unwrap_or_else(|| format!("Mix: {id}")),
311                    selected_track: -1,
312                },
313                plugin_info: serde_json::json!({ "type": "playlist", "url": format!("https://tidal.com/browse/mix/{id}"), "totalTracks": tracks.len() }),
314                tracks,
315            })
316        }
317        async fn resolve_by_isrc(&self, isrc: &str) -> LoadResult {
318            let token = match self.client.token_tracker.get_oauth_token().await {
319                Some(t) => t,
320                None => {
321                    warn!("Tidal ISRC lookup requires an OAuth token; none configured");
322                    return LoadResult::Empty {};
323                }
324            };
325            let url = format!(
326                "https://openapi.tidal.com/v2/tracks?countryCode={}",
327                self.client.country_code
328            );
329            let resp = self
330                .client
331                .base_request(self.client.inner.get(&url))
332                .query(&[("filter[isrc]", isrc)])
333                .header("Authorization", format!("Bearer {token}"))
334                .send()
335                .await;
336            let r = match resp {
337                Ok(r) => r,
338                Err(e) => {
339                    debug!("Tidal ISRC request failed: {e}");
340                    return LoadResult::Empty {};
341                }
342            };
343            if !r.status().is_success() {
344                debug!(
345                    "Tidal ISRC response {}: {}",
346                    r.status(),
347                    r.text().await.unwrap_or_default()
348                );
349                return LoadResult::Empty {};
350            }
351            let data: serde_json::Value = match r.json().await {
352                Ok(d) => d,
353                Err(e) => {
354                    debug!("Tidal ISRC response parse error: {e}");
355                    return LoadResult::Empty {};
356                }
357            };
358            match data.pointer("/data/0/id").and_then(|v| v.as_str()) {
359                Some(id) => self.get_track_data(id).await,
360                None => {
361                    debug!("Tidal ISRC={isrc} not found in catalog");
362                    LoadResult::Empty {}
363                }
364            }
365        }
366        async fn search(&self, query: &str) -> LoadResult {
367            let encoded = urlencoding::encode(query);
368            match self
369                .client
370                .get_json(&format!("/search?query={encoded}&limit=10&types=TRACKS"))
371                .await
372            {
373                Ok(data) => {
374                    let mut tracks = Vec::new();
375                    if let Some(items) = data.pointer("/tracks/items").and_then(|v| v.as_array()) {
376                        for item in items {
377                            if let Some(info) = self.parse_track(item) {
378                                tracks.push(Track::new(info));
379                            }
380                        }
381                    }
382                    if tracks.is_empty() {
383                        LoadResult::Empty {}
384                    } else {
385                        LoadResult::Search(tracks)
386                    }
387                }
388                Err(_) => LoadResult::Empty {},
389            }
390        }
391        async fn get_recommendations(&self, id: &str) -> LoadResult {
392            if let Ok(data) = self.client.get_json(&format!("/tracks/{id}")).await
393                && let Some(mix_id) = data.pointer("/mixes/TRACK_MIX").and_then(|v| v.as_str())
394            {
395                return self
396                    .get_mix(mix_id, Some("Tidal Recommendations".to_string()))
397                    .await;
398            }
399            LoadResult::Empty {}
400        }
401        async fn get_artist_top_tracks(&self, id: &str) -> LoadResult {
402            let info_data = match self.client.get_json(&format!("/artists/{id}")).await {
403                Ok(d) => d,
404                Err(_) => return LoadResult::Empty {},
405            };
406            let name = info_data
407                .get("name")
408                .and_then(|v| v.as_str())
409                .unwrap_or("Unknown Artist");
410            let limit = self.artist_load_limit.clamp(1, 10);
411            let data = match self
412                .client
413                .get_json(&format!("/artists/{id}/toptracks?limit={limit}"))
414                .await
415            {
416                Ok(d) => d,
417                Err(_) => return LoadResult::Empty {},
418            };
419            let mut tracks = Vec::new();
420            if let Some(items) = data.get("items").and_then(|v| v.as_array()) {
421                for item in items {
422                    if let Some(info) = self.parse_track(item) {
423                        tracks.push(Track::new(info));
424                    }
425                }
426            }
427            if tracks.is_empty() {
428                return LoadResult::Empty {};
429            }
430            LoadResult::Playlist(PlaylistData {
431                info: PlaylistInfo {
432                    name: format!("{name}'s Top Tracks"),
433                    selected_track: -1,
434                },
435                plugin_info: serde_json::json!({ "type": "artist", "url": format!("https://tidal.com/browse/artist/{id}"), "totalTracks": tracks.len() }),
436                tracks,
437            })
438        }
439    }
440    #[async_trait]
441    impl SourcePlugin for TidalSource {
442        fn name(&self) -> &str {
443            "tidal"
444        }
445        fn can_handle(&self, identifier: &str) -> bool {
446            self.search_prefixes()
447                .iter()
448                .any(|p| identifier.starts_with(p))
449                || self
450                    .isrc_prefixes()
451                    .iter()
452                    .any(|p| identifier.starts_with(p))
453                || self
454                    .rec_prefixes()
455                    .iter()
456                    .any(|p| identifier.starts_with(p))
457                || url_regex().is_match(identifier)
458        }
459        fn search_prefixes(&self) -> Vec<&str> {
460            vec!["tdsearch:"]
461        }
462        fn isrc_prefixes(&self) -> Vec<&str> {
463            vec!["tdisrc:"]
464        }
465        fn rec_prefixes(&self) -> Vec<&str> {
466            vec!["tdrec:"]
467        }
468        fn is_mirror(&self) -> bool {
469            false
470        }
471        async fn load(
472            &self,
473            identifier: &str,
474            _: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
475        ) -> LoadResult {
476            if let Some(prefix) = self
477                .search_prefixes()
478                .iter()
479                .find(|p| identifier.starts_with(**p))
480            {
481                return self.search(&identifier[prefix.len()..]).await;
482            }
483            if let Some(prefix) = self
484                .isrc_prefixes()
485                .iter()
486                .find(|p| identifier.starts_with(**p))
487            {
488                return self.resolve_by_isrc(&identifier[prefix.len()..]).await;
489            }
490            if let Some(prefix) = self
491                .rec_prefixes()
492                .iter()
493                .find(|p| identifier.starts_with(**p))
494            {
495                return self.get_recommendations(&identifier[prefix.len()..]).await;
496            }
497            if let Some(caps) = url_regex().captures(identifier) {
498                let type_str = caps.get(1).map_or("", |m| m.as_str());
499                let id = caps.get(2).map_or("", |m| m.as_str());
500                return match type_str {
501                    "track" => self.get_track_data(id).await,
502                    "album" | "playlist" => self.get_album_or_playlist(id, type_str).await,
503                    "mix" => self.get_mix(id, None).await,
504                    "artist" => self.get_artist_top_tracks(id).await,
505                    _ => LoadResult::Empty {},
506                };
507            }
508            LoadResult::Empty {}
509        }
510        async fn get_track(
511            &self,
512            identifier: &str,
513            _: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
514        ) -> Option<BoxedTrack> {
515            let id = if let Some(caps) = url_regex().captures(identifier) {
516                let type_str = caps.get(1).map_or("", |m| m.as_str());
517                let id = caps.get(2).map_or("", |m| m.as_str());
518                if type_str != "track" {
519                    return None;
520                }
521                id.to_owned()
522            } else {
523                identifier.to_owned()
524            };
525            let token = match self.client.token_tracker.get_oauth_token().await {
526                Some(t) => t,
527                None => {
528                    warn!("Tidal playback requires an OAuth login");
529                    return None;
530                }
531            };
532            let quality = &self.client.quality;
533            let url = format!(
534                "https://api.tidal.com/v1/tracks/{}/playbackinfo?audioquality={}&playbackmode=STREAM&assetpresentation=FULL&countryCode={}",
535                id, quality, self.client.country_code
536            );
537            debug!("Tidal: Resolving playback info for {}", id);
538            let resp = match self
539                .client
540                .inner
541                .get(&url)
542                .header("Authorization", format!("Bearer {token}"))
543                .header("User-Agent", "TIDAL/3704 CFNetwork/1220.1 Darwin/20.3.0")
544                .send()
545                .await
546            {
547                Ok(r) => r,
548                Err(e) => {
549                    warn!("Tidal: Failed to fetch playback info: {}", e);
550                    return None;
551                }
552            };
553            if !resp.status().is_success() {
554                let status = resp.status();
555                let body = resp.text().await.unwrap_or_default();
556                warn!("Tidal: Playback API returned {}: {}", status, body);
557                return None;
558            }
559            let info: PlaybackInfo = match resp.json().await {
560                Ok(i) => i,
561                Err(e) => {
562                    warn!("Tidal: Failed to parse playback info: {}", e);
563                    return None;
564                }
565            };
566            let decoded = match general_purpose::STANDARD.decode(&info.manifest) {
567                Ok(d) => d,
568                Err(e) => {
569                    warn!("Tidal: Failed to decode manifest: {}", e);
570                    return None;
571                }
572            };
573            let manifest: Manifest = match serde_json::from_slice(&decoded) {
574                Ok(m) => m,
575                Err(e) => {
576                    warn!("Tidal: Failed to parse manifest JSON: {}", e);
577                    return None;
578                }
579            };
580            let stream_url = match manifest.urls.first() {
581                Some(u) => u.clone(),
582                None => {
583                    warn!("Tidal: No stream URL in manifest");
584                    return None;
585                }
586            };
587            let mut kind = AudioFormat::from_url(&stream_url);
588            if kind == AudioFormat::Unknown {
589                if quality == "HI_RES_LOSSLESS" {
590                    kind = AudioFormat::Flac;
591                } else if quality == "LOSSLESS" {
592                    kind = AudioFormat::Mp4;
593                } else {
594                    kind = AudioFormat::Aac;
595                }
596            }
597            Some(Arc::new(TidalTrack {
598                identifier: id,
599                stream_url,
600                kind,
601                client: self.client.clone(),
602            }))
603        }
604    }
605}
606pub mod model {
607    use serde::Deserialize;
608    #[derive(Debug, Deserialize)]
609    #[serde(rename_all = "camelCase")]
610    pub struct PlaybackInfo {
611        pub manifest: String,
612        pub manifest_mime_type: String,
613    }
614    #[derive(Debug, Deserialize)]
615    #[serde(rename_all = "camelCase")]
616    pub struct Manifest {
617        pub urls: Vec<String>,
618        pub mime_type: Option<String>,
619    }
620    #[derive(Debug, Deserialize)]
621    #[serde(rename_all = "camelCase")]
622    pub struct DeviceAuthResponse {
623        pub device_code: String,
624        pub user_code: String,
625        pub verification_uri: String,
626        pub verification_uri_complete: String,
627        pub expires_in: u64,
628        pub interval: u64,
629    }
630    #[derive(Debug, Deserialize)]
631    pub struct TokenResponse {
632        pub access_token: String,
633        pub refresh_token: Option<String>,
634        pub expires_in: u64,
635    }
636    #[derive(Clone, Debug)]
637    pub struct TidalToken {
638        pub access_token: String,
639        pub expiry_ms: u64,
640    }
641    #[derive(Clone, Debug)]
642    pub enum TidalAuthToken {
643        OAuth(String),
644        Scraper(String),
645    }
646    impl TidalAuthToken {
647        pub fn value(&self) -> &str {
648            match self {
649                Self::OAuth(s) | Self::Scraper(s) => s,
650            }
651        }
652    }
653}
654pub mod oauth {
655    use super::model::{DeviceAuthResponse, TokenResponse};
656    use reqwest::Client;
657    use std::{sync::Arc, time::Duration};
658    use tokio::sync::RwLock;
659    use tracing::{error, info};
660    const CLIENT_ID: &str = "fX2JxdmntZWK0ixT";
661    const CLIENT_SECRET: &str = "1Nn9AfDAjxrgJFJbKNWLeAyKGVGmINuXPPLHVXAvxAg=";
662    pub struct TidalOAuth {
663        pub client: Client,
664        pub access_token: RwLock<Option<String>>,
665        pub refresh_token: RwLock<Option<String>>,
666    }
667    impl TidalOAuth {
668        pub fn new(refresh_token: Option<String>) -> Self {
669            Self {
670                client: Client::builder()
671                    .user_agent("TIDAL/3704 CFNetwork/1220.1 Darwin/20.3.0")
672                    .build()
673                    .unwrap_or_default(),
674                access_token: RwLock::new(None),
675                refresh_token: RwLock::new(refresh_token),
676            }
677        }
678        pub async fn get_access_token(&self) -> Option<String> {
679            if let Some(token) = &*self.access_token.read().await {
680                return Some(token.clone());
681            }
682            if self.refresh_oauth_token().await.is_ok() {
683                return self.access_token.read().await.clone();
684            }
685            None
686        }
687        pub async fn get_refresh_token(&self) -> Option<String> {
688            self.refresh_token.read().await.clone()
689        }
690        pub async fn initialize_access_token(self: Arc<Self>) {
691            if self.refresh_token.read().await.is_some() {
692                return;
693            }
694            info!("Starting Tidal device authorization flow...");
695            let form = [("client_id", CLIENT_ID), ("scope", "r_usr w_usr w_sub")];
696            let resp = match self
697                .client
698                .post("https://auth.tidal.com/v1/oauth2/device_authorization")
699                .form(&form)
700                .send()
701                .await
702            {
703                Ok(r) => r,
704                Err(e) => {
705                    error!("Tidal device authorization request failed: {}", e);
706                    return;
707                }
708            };
709            if !resp.status().is_success() {
710                error!("Tidal device authorization failed: {}", resp.status());
711                return;
712            }
713            let data: DeviceAuthResponse = match resp.json().await {
714                Ok(d) => d,
715                Err(e) => {
716                    error!("Failed to parse device auth response: {}", e);
717                    return;
718                }
719            };
720            crate::log_println!(
721                "\n  ┌────────────────────────────────────────────────────────────┐\n  │                     TIDAL OAUTH LOGIN                      │\n  ├────────────────────────────────────────────────────────────┤\n  │ 1. Visit: {:<48} │\n  │ 2. Log in and authorize the application.                   │\n  └────────────────────────────────────────────────────────────┘\n",
722                data.verification_uri_complete
723            );
724            let oauth = self.clone();
725            tokio::spawn(async move {
726                oauth.poll_token(data.device_code, data.interval).await;
727            });
728        }
729        async fn poll_token(&self, device_code: String, interval: u64) {
730            let mut interval_timer = tokio::time::interval(Duration::from_secs(interval.max(1)));
731            loop {
732                interval_timer.tick().await;
733                let form = [
734                    ("client_id", CLIENT_ID),
735                    ("device_code", &device_code),
736                    ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
737                    ("scope", "r_usr w_usr w_sub"),
738                ];
739                let resp = match self
740                    .client
741                    .post("https://auth.tidal.com/v1/oauth2/token")
742                    .basic_auth(CLIENT_ID, Some(CLIENT_SECRET))
743                    .form(&form)
744                    .send()
745                    .await
746                {
747                    Ok(r) => r,
748                    Err(_) => continue,
749                };
750                if resp.status().is_success() {
751                    if let Ok(data) = resp.json::<TokenResponse>().await {
752                        let mut at_lock = self.access_token.write().await;
753                        let mut rt_lock = self.refresh_token.write().await;
754                        *at_lock = Some(data.access_token);
755                        *rt_lock = data.refresh_token;
756                        info!("Successfully authorized Tidal OAuth");
757                        if let Some(ref rt) = *rt_lock {
758                            info!("Tidal Refresh Token: {}", rt);
759                        }
760                        return;
761                    }
762                } else if let Ok(body) = resp.json::<serde_json::Value>().await {
763                    let error = body["error"].as_str().unwrap_or_default();
764                    match error {
765                        "authorization_pending" => continue,
766                        "slow_down" => {
767                            interval_timer =
768                                tokio::time::interval(Duration::from_secs(interval + 3));
769                        }
770                        _ => {
771                            error!("Tidal OAuth polling failed: {}", error);
772                            return;
773                        }
774                    }
775                }
776            }
777        }
778        pub async fn refresh_oauth_token(&self) -> Result<(), String> {
779            let refresh_token = self.get_refresh_token().await;
780            let rt = match refresh_token {
781                Some(t) => t,
782                None => return Err("No refresh token available".to_string()),
783            };
784            info!("Refreshing Tidal OAuth token...");
785            let form = [
786                ("client_id", CLIENT_ID),
787                ("refresh_token", &rt),
788                ("grant_type", "refresh_token"),
789                ("scope", "r_usr w_usr w_sub"),
790            ];
791            let resp = self
792                .client
793                .post("https://auth.tidal.com/v1/oauth2/token")
794                .basic_auth(CLIENT_ID, Some(CLIENT_SECRET))
795                .form(&form)
796                .send()
797                .await
798                .map_err(|e| e.to_string())?;
799            if !resp.status().is_success() {
800                let status = resp.status();
801                let body = resp.text().await.unwrap_or_default();
802                error!("Tidal token refresh failed ({}): {}", status, body);
803                return Err(format!("Refresh failed: {}", status));
804            }
805            let data: TokenResponse = resp.json().await.map_err(|e| e.to_string())?;
806            let mut at_lock = self.access_token.write().await;
807            let mut rt_lock = self.refresh_token.write().await;
808            *at_lock = Some(data.access_token);
809            if data.refresh_token.is_some() {
810                *rt_lock = data.refresh_token;
811            }
812            info!("Successfully refreshed Tidal OAuth token");
813            Ok(())
814        }
815    }
816}
817pub mod token {
818    use super::{model::TidalToken, oauth::TidalOAuth};
819    use regex::Regex;
820    use std::sync::{Arc, OnceLock};
821    use tokio::sync::RwLock;
822    use tracing::{error, info};
823    fn script_regex() -> &'static Regex {
824        static RE: OnceLock<Regex> = OnceLock::new();
825        RE.get_or_init(|| Regex::new(r#"src="(/assets/index-[^"]+\.js)""#).unwrap())
826    }
827    fn client_id_regex() -> &'static Regex {
828        static RE: OnceLock<Regex> = OnceLock::new();
829        RE.get_or_init(|| Regex::new(r#"clientId\s*[:=]\s*"([^"]+)""#).unwrap())
830    }
831    pub struct TidalTokenTracker {
832        pub token: RwLock<Option<TidalToken>>,
833        pub client: Arc<reqwest::Client>,
834        pub oauth: Arc<TidalOAuth>,
835    }
836    impl TidalTokenTracker {
837        pub fn new(client: Arc<reqwest::Client>, oauth: Arc<TidalOAuth>) -> Self {
838            Self {
839                token: RwLock::new(None),
840                client,
841                oauth,
842            }
843        }
844        pub async fn get_scraper_token(&self) -> Option<String> {
845            {
846                let lock = self.token.read().await;
847                if let Some(token) = &*lock
848                    && self.is_valid(token)
849                {
850                    return Some(token.access_token.clone());
851                }
852            }
853            self.refresh_token().await
854        }
855        pub async fn get_oauth_token(&self) -> Option<String> {
856            self.oauth.get_access_token().await
857        }
858        fn is_valid(&self, token: &TidalToken) -> bool {
859            let now = std::time::SystemTime::now()
860                .duration_since(std::time::UNIX_EPOCH)
861                .unwrap()
862                .as_millis() as u64;
863            token.expiry_ms > now + 10_000
864        }
865        async fn refresh_token(&self) -> Option<String> {
866            info!("Fetching new Tidal API token via scraper...");
867            let listen_url = "https://listen.tidal.com";
868            let resp = self.client.get(listen_url).send().await.ok()?;
869            if !resp.status().is_success() {
870                error!("Tidal listen page returned status: {}", resp.status());
871                return None;
872            }
873            let html = resp.text().await.unwrap_or_default();
874            let script_path = script_regex().captures(&html)?.get(1)?.as_str();
875            let script_url = format!("https://listen.tidal.com{}", script_path);
876            let js_resp = self.client.get(&script_url).send().await.ok()?;
877            let js_content = js_resp.text().await.unwrap_or_default();
878            let mut matches = client_id_regex().captures_iter(&js_content);
879            matches.next();
880            let token_str = matches.next()?.get(1)?.as_str().to_owned();
881            let now = std::time::SystemTime::now()
882                .duration_since(std::time::UNIX_EPOCH)
883                .unwrap()
884                .as_millis() as u64;
885            let token = TidalToken {
886                access_token: token_str.clone(),
887                expiry_ms: now + (24 * 60 * 60 * 1000),
888            };
889            let mut lock = self.token.write().await;
890            *lock = Some(token);
891            info!("Successfully refreshed Tidal scraper token");
892            Some(token_str)
893        }
894        pub fn init(self: Arc<Self>) {
895            let this = self.clone();
896            tokio::spawn(async move {
897                this.get_scraper_token().await;
898            });
899        }
900        pub async fn has_oauth_refresh_token(&self) -> bool {
901            self.oauth.get_refresh_token().await.is_some()
902        }
903    }
904}
905pub mod track {
906    use super::client::TidalClient;
907    use crate::{
908        audio::source::HttpSource,
909        common::types::AudioFormat,
910        sources::playable_track::{PlayableTrack, ResolvedTrack},
911    };
912    use async_trait::async_trait;
913    use std::sync::Arc;
914    use tracing::debug;
915    pub struct TidalTrack {
916        pub identifier: String,
917        pub stream_url: String,
918        pub kind: AudioFormat,
919        pub client: Arc<TidalClient>,
920    }
921    #[async_trait]
922    impl PlayableTrack for TidalTrack {
923        async fn resolve(&self) -> Result<ResolvedTrack, String> {
924            debug!(
925                "TidalTrack: resolving {} with quality {}",
926                self.identifier, self.client.quality
927            );
928            let client_inner = (*self.client.inner).clone();
929            let stream_url = self.stream_url.clone();
930            let kind = self.kind;
931            let reader = HttpSource::new(client_inner, &stream_url)
932                .await
933                .map(|r| Box::new(r) as Box<dyn symphonia::core::io::MediaSource>)
934                .map_err(|e| format!("Failed to initialize source: {e}"))?;
935            Ok(ResolvedTrack::new(reader, Some(kind)))
936        }
937    }
938}
939pub use manager::TidalSource;