Skip to main content

lavende_core/sources/
deezer.rs

1pub mod helpers {
2    use super::{DeezerSource, PUBLIC_API_BASE};
3    use serde_json::Value;
4    impl DeezerSource {
5        pub(crate) async fn get_json_public(&self, path: &str) -> Option<Value> {
6            let url = format!("{PUBLIC_API_BASE}/{path}");
7            match self.client.get(&url).send().await {
8                Ok(res) => {
9                    if res.status().is_success() {
10                        res.json().await.ok()
11                    } else {
12                        tracing::warn!(
13                            "Deezer public API request failed: {url} (Status: {})",
14                            res.status()
15                        );
16                        None
17                    }
18                }
19                Err(e) => {
20                    tracing::error!(
21                        "Deezer public API request error: {url} (Error: {e}). If this is a connectivity error, check your proxy settings.",
22                    );
23                    None
24                }
25            }
26        }
27    }
28}
29pub mod metadata {
30    use super::DeezerSource;
31    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo, Track};
32    impl DeezerSource {
33        pub(crate) async fn get_track_by_isrc(&self, isrc: &str) -> Option<Track> {
34            let url = format!("track/isrc:{isrc}");
35            tracing::debug!("DeezerSource: Fetching metadata for ISRC: {isrc} (URL: {url})");
36            let json = self.get_json_public(&url).await?;
37            if json.get("id").is_some() {
38                let res = self.parse_track(&json);
39                if let Some(ref t) = res {
40                    tracing::debug!(
41                        "DeezerSource: Found track for ISRC {isrc}: {}",
42                        t.info.identifier
43                    );
44                } else {
45                    tracing::debug!("DeezerSource: Failed to parse track for ISRC {isrc}");
46                }
47                res
48            } else {
49                tracing::debug!("DeezerSource: No track found for ISRC {isrc}");
50                None
51            }
52        }
53        pub(crate) async fn get_album(&self, id: &str) -> LoadResult {
54            let json = match self.get_json_public(&format!("album/{id}")).await {
55                Some(j) => j,
56                None => return LoadResult::Empty {},
57            };
58            let tracks_json = match self
59                .get_json_public(&format!("album/{id}/tracks?limit=10000"))
60                .await
61            {
62                Some(j) => j,
63                None => return LoadResult::Empty {},
64            };
65            let mut tracks = Vec::new();
66            let artwork_url = json
67                .get("cover_xl")
68                .and_then(|v| v.as_str())
69                .map(|s| s.to_owned());
70            if let Some(data) = tracks_json.get("data").and_then(|d| d.as_array()) {
71                for item in data {
72                    if let Some(mut track) = self.parse_track(item) {
73                        if track.info.artwork_url.is_none() {
74                            track.info.artwork_url = artwork_url.clone();
75                        }
76                        tracks.push(track);
77                    }
78                }
79            }
80            if tracks.is_empty() {
81                return LoadResult::Empty {};
82            }
83            LoadResult::Playlist(PlaylistData {
84                info: PlaylistInfo {
85                    name: json
86                        .get("title")
87                        .and_then(|v| v.as_str())
88                        .unwrap_or("Unknown Album")
89                        .to_owned(),
90                    selected_track: -1,
91                },
92                plugin_info: serde_json::json!({
93                  "type": "album",
94                  "url": format!("https://www.deezer.com/album/{id}"),
95                  "artworkUrl": json.get("cover_xl").and_then(|v| v.as_str()),
96                  "author": json.get("artist").and_then(|v| v.get("name")).and_then(|v| v.as_str()),
97                  "totalTracks": json.get("nb_tracks").and_then(|v| v.as_u64()).unwrap_or(tracks.len() as u64)
98                }),
99                tracks,
100            })
101        }
102        pub(crate) async fn get_playlist(&self, id: &str) -> LoadResult {
103            let json = match self.get_json_public(&format!("playlist/{id}")).await {
104                Some(j) => j,
105                None => return LoadResult::Empty {},
106            };
107            let tracks_json = match self
108                .get_json_public(&format!("playlist/{id}/tracks?limit=10000"))
109                .await
110            {
111                Some(j) => j,
112                None => return LoadResult::Empty {},
113            };
114            let mut tracks = Vec::new();
115            if let Some(data) = tracks_json.get("data").and_then(|d| d.as_array()) {
116                for item in data {
117                    if let Some(track) = self.parse_track(item) {
118                        tracks.push(track);
119                    }
120                }
121            }
122            if tracks.is_empty() {
123                return LoadResult::Empty {};
124            }
125            LoadResult::Playlist(PlaylistData {
126                info: PlaylistInfo {
127                    name: json
128                        .get("title")
129                        .and_then(|v| v.as_str())
130                        .unwrap_or("Unknown Playlist")
131                        .to_owned(),
132                    selected_track: -1,
133                },
134                plugin_info: serde_json::json!({
135                  "type": "playlist",
136                  "url": format!("https://www.deezer.com/playlist/{id}"),
137                  "artworkUrl": json.get("picture_xl").and_then(|v| v.as_str()),
138                  "author": json.get("creator").and_then(|v| v.get("name")).and_then(|v| v.as_str()),
139                  "totalTracks": json.get("nb_tracks").and_then(|v| v.as_u64()).unwrap_or(tracks.len() as u64)
140                }),
141                tracks,
142            })
143        }
144        pub(crate) async fn get_artist(&self, id: &str) -> LoadResult {
145            let json = match self.get_json_public(&format!("artist/{id}")).await {
146                Some(j) => j,
147                None => return LoadResult::Empty {},
148            };
149            let tracks_json = match self
150                .get_json_public(&format!("artist/{id}/top?limit=50"))
151                .await
152            {
153                Some(j) => j,
154                None => return LoadResult::Empty {},
155            };
156            let artwork_url = json
157                .get("picture_xl")
158                .and_then(|v| v.as_str())
159                .map(|s| s.to_owned());
160            let author = json
161                .get("name")
162                .and_then(|v| v.as_str())
163                .unwrap_or("Unknown Artist")
164                .to_owned();
165            let mut tracks = Vec::new();
166            if let Some(data) = tracks_json.get("data").and_then(|d| d.as_array()) {
167                for item in data {
168                    if let Some(mut track) = self.parse_track(item) {
169                        if track.info.artwork_url.is_none() {
170                            track.info.artwork_url = artwork_url.clone();
171                        }
172                        tracks.push(track);
173                    }
174                }
175            }
176            if tracks.is_empty() {
177                return LoadResult::Empty {};
178            }
179            LoadResult::Playlist(PlaylistData {
180                info: PlaylistInfo {
181                    name: format!("{author}'s Top Tracks"),
182                    selected_track: -1,
183                },
184                plugin_info: serde_json::json!({
185                  "type": "artist",
186                  "url": format!("https://www.deezer.com/artist/{id}"),
187                  "artworkUrl": json.get("picture_xl").and_then(|v| v.as_str()),
188                  "author": author,
189                  "totalTracks": tracks.len()
190                }),
191                tracks,
192            })
193        }
194    }
195}
196pub mod parser {
197    use super::DeezerSource;
198    use crate::protocol::tracks::{Track, TrackInfo};
199    use serde_json::{Value, json};
200    impl DeezerSource {
201        pub(crate) fn parse_track(&self, json: &Value) -> Option<Track> {
202            let id = json.get("id").and_then(|v| {
203                v.as_str()
204                    .map(|s| s.to_owned())
205                    .or_else(|| v.as_i64().map(|n| n.to_string()))
206            })?;
207            let title = json.get("title")?.as_str()?.to_owned();
208            let artist = json.get("artist")?.get("name")?.as_str()?.to_owned();
209            let duration = json.get("duration")?.as_u64()? * 1000;
210            if let Some(readable) = json.get("readable").and_then(|v| v.as_bool())
211                && !readable
212            {
213                let countries = json
214                    .get("available_countries")
215                    .and_then(|v| {
216                        v.as_array()
217                            .map(|a| {
218                                a.iter()
219                                    .filter_map(|c| c.as_str())
220                                    .collect::<Vec<_>>()
221                                    .join(", ")
222                            })
223                            .or_else(|| v.as_str().map(|s| s.to_owned()))
224                    })
225                    .unwrap_or_default();
226                tracing::debug!(
227                    "Deezer track {} ({}) is marked as not readable. Available countries: {}. It might fail unless a fallback is found.",
228                    title,
229                    id,
230                    countries
231                );
232            }
233            let isrc = json
234                .get("isrc")
235                .and_then(|v| v.as_str())
236                .filter(|s| !s.is_empty())
237                .map(|s| s.to_owned());
238            let artwork_url = json
239                .get("album")
240                .and_then(|a| a.get("cover_xl"))
241                .and_then(|v| v.as_str())
242                .filter(|s| !s.is_empty())
243                .map(|s| s.to_owned())
244                .or_else(|| {
245                    json.get("md5_image").and_then(|v| v.as_str()).map(|id| {
246                    format!(
247                        "https://cdn-images.dzcdn.net/images/cover/{id}/1000x1000-000000-80-0-0.jpg"
248                    )
249                })
250                });
251            let uri = json
252                .get("link")
253                .and_then(|v| v.as_str())
254                .filter(|s| !s.is_empty())
255                .map(|s| s.to_owned());
256            let mut track = Track::new(TrackInfo {
257                identifier: id,
258                is_seekable: true,
259                author: artist,
260                length: duration,
261                is_stream: false,
262                position: 0,
263                title,
264                uri: uri.clone(),
265                artwork_url,
266                isrc,
267                source_name: "deezer".to_owned(),
268            });
269            let album_name = json.pointer("/album/title").and_then(|v| v.as_str());
270            let album_url = json
271                .pointer("/album/id")
272                .and_then(|v| {
273                    v.as_str()
274                        .map(|s| s.to_owned())
275                        .or_else(|| v.as_u64().map(|id| id.to_string()))
276                })
277                .map(|id| format!("https://www.deezer.com/album/{id}"));
278            let artist_url = json
279                .pointer("/artist/id")
280                .and_then(|v| {
281                    v.as_str()
282                        .map(|s| s.to_owned())
283                        .or_else(|| v.as_u64().map(|id| id.to_string()))
284                })
285                .map(|id| format!("https://www.deezer.com/artist/{id}"));
286            let artist_artwork_url = json.pointer("/artist/picture_xl").and_then(|v| v.as_str());
287            let preview_url = json.get("preview").and_then(|v| v.as_str());
288            track.plugin_info = json!({
289                "albumName": album_name,
290                "albumUrl": album_url,
291                "artistUrl": artist_url,
292                "artistArtworkUrl": artist_artwork_url,
293                "previewUrl": preview_url,
294                "isPreview": false
295            });
296            Some(track)
297        }
298        pub(crate) fn parse_recommendation_track(&self, json: &Value) -> Option<Track> {
299            let id = json.get("SNG_ID").and_then(|v| {
300                v.as_str()
301                    .map(|s| s.to_owned())
302                    .or_else(|| v.as_i64().map(|n| n.to_string()))
303            })?;
304            let title = json.get("SNG_TITLE")?.as_str()?.to_owned();
305            let artist = json.get("ART_NAME")?.as_str()?.to_owned();
306            let duration = json.get("DURATION")?.as_u64()? * 1000;
307            if let Some(readable) = json.get("READABLE").and_then(|v| v.as_bool())
308                && !readable
309            {
310                tracing::debug!(
311                    "Deezer recommendation track {} ({}) is marked as not readable. It might fail unless a fallback is found.",
312                    title,
313                    id
314                );
315            }
316            let isrc = json
317                .get("ISRC")
318                .and_then(|v| v.as_str())
319                .map(|s| s.to_owned());
320            let album_pic = json
321                .get("ALB_PICTURE")
322                .and_then(|v| v.as_str())
323                .unwrap_or("");
324            let artwork_url = if !album_pic.is_empty() {
325                Some(format!(
326                    "https://cdn-images.dzcdn.net/images/cover/{album_pic}/1000x1000-000000-80-0-0.jpg"
327                ))
328            } else {
329                None
330            };
331            let uri_val = Some(format!("https://deezer.com/track/{id}"));
332            let mut track = Track::new(TrackInfo {
333                identifier: id.clone(),
334                is_seekable: true,
335                author: artist,
336                length: duration,
337                is_stream: false,
338                position: 0,
339                title,
340                uri: uri_val.clone(),
341                artwork_url,
342                isrc,
343                source_name: "deezer".to_owned(),
344            });
345            let album_name = json.get("ALB_TITLE").and_then(|v| v.as_str());
346            let album_url = json
347                .get("ALB_ID")
348                .and_then(|v| {
349                    v.as_str()
350                        .map(|s| s.to_owned())
351                        .or_else(|| v.as_u64().map(|id| id.to_string()))
352                })
353                .map(|id| format!("https://www.deezer.com/album/{id}"));
354            let artist_url = json
355                .get("ART_ID")
356                .and_then(|v| {
357                    v.as_str()
358                        .map(|s| s.to_owned())
359                        .or_else(|| v.as_u64().map(|id| id.to_string()))
360                })
361                .map(|id| format!("https://www.deezer.com/artist/{id}"));
362            let artist_artwork_url = json
363                .pointer("/ARTISTS/0/ART_PICTURE")
364                .and_then(|v| v.as_str())
365                .map(|id| {
366                    format!(
367                        "https://cdn-images.dzcdn.net/images/cover/{id}/1000x1000-000000-80-0-0.jpg"
368                    )
369                });
370            let preview_url = json.pointer("/MEDIA/0/HREF").and_then(|v| v.as_str());
371            track.plugin_info = json!({
372                "albumName": album_name,
373                "albumUrl": album_url,
374                "artistUrl": artist_url,
375                "artistArtworkUrl": artist_artwork_url,
376                "previewUrl": preview_url,
377                "isPreview": false
378            });
379            Some(track)
380        }
381    }
382}
383pub mod recommendations {
384    use super::{DeezerSource, PRIVATE_API_BASE};
385    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo, Track};
386    use serde_json::Value;
387    impl DeezerSource {
388        pub(crate) async fn get_recommendations(&self, query: &str) -> LoadResult {
389            let tokens = match self.token_tracker.get_token().await {
390                Some(t) => t,
391                None => return LoadResult::Empty {},
392            };
393            let (method, payload) =
394                if let Some(artist_id) = query.strip_prefix(super::REC_ARTIST_PREFIX) {
395                    (
396                        "song.getSmartRadio",
397                        serde_json::json!({ "art_id": artist_id }),
398                    )
399                } else {
400                    let track_id = query.strip_prefix(super::REC_TRACK_PREFIX).unwrap_or(query);
401                    (
402                        "song.getSearchTrackMix",
403                        serde_json::json!({ "sng_id": track_id, "start_with_input_track": "true" }),
404                    )
405                };
406            let url = format!(
407                "{PRIVATE_API_BASE}?method={method}&input=3&api_version=1.0&api_token={}",
408                tokens.api_token
409            );
410            let res = match self
411                .client
412                .post(&url)
413                .header(
414                    "Cookie",
415                    format!(
416                        "sid={}; dzr_uniq_id={}",
417                        tokens.session_id, tokens.dzr_uniq_id
418                    ),
419                )
420                .json(&payload)
421                .send()
422                .await
423            {
424                Ok(r) => r,
425                Err(_) => return LoadResult::Empty {},
426            };
427            let json: Value = res.json().await.unwrap_or(Value::Null);
428            let data = json.get("results").and_then(|r| r.get("data"));
429            let tracks: Vec<Track> = if let Some(arr) = data.and_then(|d| d.as_array()) {
430                arr.iter()
431                    .filter_map(|item| self.parse_recommendation_track(item))
432                    .collect()
433            } else if let Some(obj) = data.and_then(|d| d.as_object()) {
434                obj.values()
435                    .filter_map(|item| self.parse_recommendation_track(item))
436                    .collect()
437            } else {
438                Vec::new()
439            };
440            if tracks.is_empty() {
441                return LoadResult::Empty {};
442            }
443            LoadResult::Playlist(PlaylistData {
444                info: PlaylistInfo {
445                    name: "Deezer Recommendations".to_owned(),
446                    selected_track: -1,
447                },
448                plugin_info: serde_json::json!({
449                  "type": "recommendations",
450                  "totalTracks": tracks.len()
451                }),
452                tracks,
453            })
454        }
455    }
456}
457pub mod search {
458    use super::DeezerSource;
459    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo, Track};
460    impl DeezerSource {
461        pub(crate) async fn search(&self, query: &str) -> LoadResult {
462            let url = format!("search?q={}", urlencoding::encode(query));
463            if let Some(json) = self.get_json_public(&url).await
464                && let Some(data) = json.get("data").and_then(|v| v.as_array())
465            {
466                if data.is_empty() {
467                    return LoadResult::Empty {};
468                }
469                let tracks: Vec<Track> = data
470                    .iter()
471                    .filter_map(|item| self.parse_track(item))
472                    .collect();
473                if tracks.is_empty() {
474                    return LoadResult::Empty {};
475                }
476                return LoadResult::Search(tracks);
477            }
478            LoadResult::Empty {}
479        }
480        pub(crate) async fn get_autocomplete(
481            &self,
482            query: &str,
483            types: &[String],
484        ) -> Option<crate::protocol::tracks::SearchResult> {
485            let url = format!("search/autocomplete?q={}", urlencoding::encode(query));
486            let json = self.get_json_public(&url).await?;
487            let all_types = types.is_empty();
488            let mut tracks = Vec::new();
489            let mut albums = Vec::new();
490            let mut artists = Vec::new();
491            let mut playlists = Vec::new();
492            let texts = Vec::new();
493            if (all_types || types.contains(&"album".to_owned()))
494                && let Some(data) = json.pointer("/albums/data").and_then(|v| v.as_array())
495            {
496                for album in data {
497                    let title = album
498                        .get("title")
499                        .and_then(|v| v.as_str())
500                        .unwrap_or("Unknown Album")
501                        .to_owned();
502                    let link = album
503                        .get("link")
504                        .and_then(|v| v.as_str())
505                        .unwrap_or("")
506                        .to_owned();
507                    let cover_xl = album
508                        .get("cover_xl")
509                        .and_then(|v| v.as_str())
510                        .unwrap_or("")
511                        .to_owned();
512                    let artist_name = album
513                        .pointer("/artist/name")
514                        .and_then(|v| v.as_str())
515                        .unwrap_or("Unknown Artist")
516                        .to_owned();
517                    let nb_tracks = album.get("nb_tracks").and_then(|v| v.as_u64()).unwrap_or(0);
518                    albums.push(PlaylistData {
519                        info: PlaylistInfo {
520                            name: title,
521                            selected_track: -1,
522                        },
523                        plugin_info: serde_json::json!({
524                          "type": "album",
525                          "url": link,
526                          "artworkUrl": if cover_xl.is_empty() { None } else { Some(cover_xl) },
527                          "author": artist_name,
528                          "totalTracks": nb_tracks
529                        }),
530                        tracks: Vec::new(),
531                    });
532                }
533            }
534            if (all_types || types.contains(&"artist".to_owned()))
535                && let Some(data) = json.pointer("/artists/data").and_then(|v| v.as_array())
536            {
537                for artist in data {
538                    let name = artist
539                        .get("name")
540                        .and_then(|v| v.as_str())
541                        .unwrap_or("Unknown Artist")
542                        .to_owned();
543                    let link = artist
544                        .get("link")
545                        .and_then(|v| v.as_str())
546                        .unwrap_or("")
547                        .to_owned();
548                    let picture_xl = artist
549                        .get("picture_xl")
550                        .and_then(|v| v.as_str())
551                        .unwrap_or("")
552                        .to_owned();
553                    artists.push(PlaylistData {
554                        info: PlaylistInfo {
555                            name: format!("{name}'s Top Tracks"),
556                            selected_track: -1,
557                        },
558                        plugin_info: serde_json::json!({
559                          "type": "artist",
560                          "url": link,
561                          "artworkUrl": if picture_xl.is_empty() { None } else { Some(picture_xl) },
562                          "author": name,
563                          "totalTracks": 0
564                        }),
565                        tracks: Vec::new(),
566                    });
567                }
568            }
569            if (all_types || types.contains(&"playlist".to_owned()))
570                && let Some(data) = json.pointer("/playlists/data").and_then(|v| v.as_array())
571            {
572                for playlist in data {
573                    let title = playlist
574                        .get("title")
575                        .and_then(|v| v.as_str())
576                        .unwrap_or("Unknown Playlist")
577                        .to_owned();
578                    let link = playlist
579                        .get("link")
580                        .and_then(|v| v.as_str())
581                        .unwrap_or("")
582                        .to_owned();
583                    let picture_xl = playlist
584                        .get("picture_xl")
585                        .and_then(|v| v.as_str())
586                        .unwrap_or("")
587                        .to_owned();
588                    let creator_name = playlist
589                        .pointer("/creator/name")
590                        .and_then(|v| v.as_str())
591                        .unwrap_or("Unknown Creator")
592                        .to_owned();
593                    let nb_tracks = playlist
594                        .get("nb_tracks")
595                        .and_then(|v| v.as_u64())
596                        .unwrap_or(0);
597                    playlists.push(PlaylistData {
598                        info: PlaylistInfo {
599                            name: title,
600                            selected_track: -1,
601                        },
602                        plugin_info: serde_json::json!({
603                          "type": "playlist",
604                          "url": link,
605                          "artworkUrl": if picture_xl.is_empty() { None } else { Some(picture_xl) },
606                          "author": creator_name,
607                          "totalTracks": nb_tracks
608                        }),
609                        tracks: Vec::new(),
610                    });
611                }
612            }
613            if (all_types || types.contains(&"track".to_owned()))
614                && let Some(data) = json.pointer("/tracks/data").and_then(|v| v.as_array())
615            {
616                for track in data {
617                    if let Some(parsed) = self.parse_track(track) {
618                        tracks.push(parsed);
619                    }
620                }
621            }
622            Some(crate::protocol::tracks::SearchResult {
623                tracks,
624                albums,
625                artists,
626                playlists,
627                texts,
628                plugin: serde_json::json!({}),
629            })
630        }
631    }
632}
633pub mod token {
634    use crate::common::types::Shared;
635    use serde_json::Value;
636    use std::{
637        sync::{
638            Arc,
639            atomic::{AtomicUsize, Ordering},
640        },
641        time::{Duration, Instant},
642    };
643    use tracing::{debug, error};
644    #[derive(Debug, Clone)]
645    pub struct DeezerTokens {
646        pub session_id: String,
647        pub dzr_uniq_id: String,
648        pub api_token: String,
649        pub license_token: String,
650        pub expire_at: Instant,
651        pub arl_index: usize,
652    }
653    pub struct DeezerTokenTracker {
654        client: Arc<reqwest::Client>,
655        arls: Vec<String>,
656        tokens: Shared<Vec<Option<DeezerTokens>>>,
657        current_index: AtomicUsize,
658    }
659    impl DeezerTokenTracker {
660        pub fn new(client: Arc<reqwest::Client>, arls: Vec<String>) -> Self {
661            let size = arls.len();
662            Self {
663                client,
664                arls,
665                tokens: Arc::new(tokio::sync::Mutex::new(vec![None; size])),
666                current_index: AtomicUsize::new(0),
667            }
668        }
669        pub async fn get_token(&self) -> Option<DeezerTokens> {
670            let index = self.current_index.fetch_add(1, Ordering::Relaxed) % self.arls.len();
671            self.get_token_at(index).await
672        }
673        pub async fn get_token_at(&self, index: usize) -> Option<DeezerTokens> {
674            {
675                let guard = self.tokens.lock().await;
676                if let Some(tokens) = &guard[index]
677                    && Instant::now() < tokens.expire_at
678                {
679                    return Some(tokens.clone());
680                }
681            }
682            self.refresh_session(index).await
683        }
684        pub async fn invalidate_token(&self, index: usize) {
685            let mut guard = self.tokens.lock().await;
686            guard[index] = None;
687        }
688        async fn refresh_session(&self, index: usize) -> Option<DeezerTokens> {
689            let arl = &self.arls[index];
690            let initial_cookie = format!("arl={arl}");
691            let url = "https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=";
692            let req = self.client.get(url).header("Cookie", initial_cookie);
693            let resp = match req.send().await {
694                Ok(r) => r,
695                Err(e) => {
696                    error!("DeezerTokenTracker: Failed to refresh session (index {index}): {e}");
697                    return None;
698                }
699            };
700            let mut session_id = String::new();
701            let mut dzr_uniq_id = String::new();
702            for cookie in resp.cookies() {
703                match cookie.name() {
704                    "sid" => session_id = cookie.value().to_owned(),
705                    "dzr_uniq_id" => dzr_uniq_id = cookie.value().to_owned(),
706                    _ => {}
707                }
708            }
709            let body: Value = match resp.json().await {
710                Ok(v) => v,
711                Err(e) => {
712                    error!("DeezerTokenTracker: Failed to parse session response: {e}");
713                    return None;
714                }
715            };
716            let api_token = body
717                .get("results")
718                .and_then(|r| r.get("checkForm"))
719                .and_then(|v| v.as_str())
720                .map(|s| s.to_owned())?;
721            let license_token = body
722                .get("results")
723                .and_then(|r| r.get("USER"))
724                .and_then(|u| u.get("OPTIONS"))
725                .and_then(|o| o.get("license_token"))
726                .and_then(|v| v.as_str())
727                .map(|s| s.to_owned())
728                .unwrap_or_default();
729            let tokens = DeezerTokens {
730                session_id,
731                dzr_uniq_id,
732                api_token,
733                license_token,
734                expire_at: Instant::now() + Duration::from_secs(3600),
735                arl_index: index,
736            };
737            {
738                let mut guard = self.tokens.lock().await;
739                guard[index] = Some(tokens.clone());
740            }
741            debug!("DeezerTokenTracker: Refreshed tokens for index {index}");
742            Some(tokens)
743        }
744    }
745}
746pub mod track {
747    use crate::{
748        common::types::AudioFormat,
749        config::HttpProxyConfig,
750        sources::{
751            deezer::{reader::DeezerReader, token::DeezerTokenTracker},
752            playable_track::{PlayableTrack, ResolvedTrack},
753        },
754    };
755    use async_trait::async_trait;
756    use std::{net::IpAddr, sync::Arc};
757    use tracing::{debug, error, warn};
758    pub struct DeezerTrack {
759        pub client: Arc<reqwest::Client>,
760        pub track_id: String,
761        pub token_tracker: Arc<DeezerTokenTracker>,
762        pub master_key: String,
763        pub local_addr: Option<IpAddr>,
764        pub proxy: Option<HttpProxyConfig>,
765    }
766    struct ResolvedUrl {
767        cdn_url: String,
768        track_id: String,
769        arl_index: usize,
770    }
771    #[async_trait]
772    impl PlayableTrack for DeezerTrack {
773        fn supports_seek(&self) -> bool {
774            true
775        }
776        async fn resolve(&self) -> Result<ResolvedTrack, String> {
777            const MAX_RETRIES: u32 = 3;
778            let mut last_arl: Option<usize> = None;
779            for attempt in 0..=MAX_RETRIES {
780                if attempt > 0
781                    && let Some(idx) = last_arl.take()
782                {
783                    self.token_tracker.invalidate_token(idx).await;
784                }
785                let resolved = match resolve_cdn_url(
786                    &self.client,
787                    &self.token_tracker,
788                    &self.track_id,
789                )
790                .await
791                {
792                    Some(r) => r,
793                    None => continue,
794                };
795                last_arl = Some(resolved.arl_index);
796                let master_key = self.master_key.clone();
797                let local_addr = self.local_addr;
798                let proxy = self.proxy.clone();
799                let cdn_url = resolved.cdn_url.clone();
800                let effective_id = resolved.track_id.clone();
801                let reader_result =
802                    DeezerReader::new(&cdn_url, &effective_id, &master_key, local_addr, proxy)
803                        .await
804                        .map(|r| {
805                            (
806                                Box::new(r) as Box<dyn symphonia::core::io::MediaSource>,
807                                cdn_url,
808                            )
809                        })
810                        .map_err(|e| e.to_string());
811                let (reader, final_url) = match reader_result {
812                    Ok(v) => v,
813                    Err(e) => {
814                        warn!(
815                            "Deezer CDN open failed for {} (attempt {}/{}): {e} — rotating ARL",
816                            self.track_id,
817                            attempt + 1,
818                            MAX_RETRIES + 1,
819                        );
820                        continue;
821                    }
822                };
823                let hint = AudioFormat::from_url(&final_url);
824                return Ok(ResolvedTrack::new(reader, Some(hint)));
825            }
826            error!("Deezer: all retries exhausted for {}", self.track_id);
827            Err("Failed to open Deezer stream after retries".to_string())
828        }
829    }
830    async fn resolve_cdn_url(
831        client: &Arc<reqwest::Client>,
832        token_tracker: &Arc<DeezerTokenTracker>,
833        track_id: &str,
834    ) -> Option<ResolvedUrl> {
835        let tokens = token_tracker.get_token().await?;
836        let arl_index = tokens.arl_index;
837        let song_url = format!(
838            "https://www.deezer.com/ajax/gw-light.php?method=song.getData&input=3&api_version=1.0&api_token={}",
839            tokens.api_token
840        );
841        let json: serde_json::Value = match client
842            .post(&song_url)
843            .header(
844                "Cookie",
845                format!(
846                    "sid={}; dzr_uniq_id={}",
847                    tokens.session_id, tokens.dzr_uniq_id
848                ),
849            )
850            .json(&serde_json::json!({ "sng_id": track_id }))
851            .send()
852            .await
853        {
854            Ok(r) => match r.json().await {
855                Ok(v) => v,
856                Err(_) => {
857                    token_tracker.invalidate_token(arl_index).await;
858                    return None;
859                }
860            },
861            Err(e) => {
862                debug!("Deezer: song.getData failed: {e}");
863                token_tracker.invalidate_token(arl_index).await;
864                return None;
865            }
866        };
867        if json
868            .get("error")
869            .and_then(|v| v.as_array())
870            .is_some_and(|e| !e.is_empty())
871        {
872            debug!("Deezer: API error in song.getData");
873            token_tracker.invalidate_token(arl_index).await;
874            return None;
875        }
876        let mut results = json.get("results")?.clone();
877        let rights = results.get("RIGHTS");
878        if is_rights_empty(rights)
879            && let Some(fallback) = results.get("FALLBACK")
880            && !fallback
881                .get("TRACK_TOKEN")
882                .map(|v| v.is_null())
883                .unwrap_or(true)
884        {
885            let fallback_id = fallback.get("SNG_ID").and_then(|v| {
886                v.as_str()
887                    .map(|s| s.to_owned())
888                    .or_else(|| v.as_i64().map(|n| n.to_string()))
889            });
890            if let Some(id) = fallback_id {
891                debug!("Deezer: track {track_id} has no RIGHTS, using FALLBACK {id}");
892                results = fallback.clone();
893                let track_token = results.get("TRACK_TOKEN").and_then(|v| v.as_str())?;
894                return fetch_media_url(
895                    client,
896                    token_tracker,
897                    &tokens,
898                    track_token,
899                    &id,
900                    arl_index,
901                )
902                .await;
903            } else {
904                warn!("Deezer: track {track_id} FALLBACK SNG_ID has unexpected format");
905            }
906        }
907        let track_token = results.get("TRACK_TOKEN").and_then(|v| v.as_str())?;
908        fetch_media_url(
909            client,
910            token_tracker,
911            &tokens,
912            track_token,
913            track_id,
914            arl_index,
915        )
916        .await
917    }
918    async fn fetch_media_url(
919        client: &Arc<reqwest::Client>,
920        token_tracker: &Arc<DeezerTokenTracker>,
921        tokens: &crate::sources::deezer::token::DeezerTokens,
922        track_token: &str,
923        effective_track_id: &str,
924        arl_index: usize,
925    ) -> Option<ResolvedUrl> {
926        let body = serde_json::json!({
927            "license_token": tokens.license_token,
928            "media": [{ "type": "FULL", "formats": [
929                { "cipher": "BF_CBC_STRIPE", "format": "MP3_128" },
930                { "cipher": "BF_CBC_STRIPE", "format": "MP3_64" }
931            ]}],
932            "track_tokens": [track_token]
933        });
934        let json: serde_json::Value = match client
935            .post("https://media.deezer.com/v1/get_url")
936            .json(&body)
937            .send()
938            .await
939        {
940            Ok(r) => match r.json().await {
941                Ok(v) => v,
942                Err(_) => {
943                    token_tracker.invalidate_token(arl_index).await;
944                    return None;
945                }
946            },
947            Err(e) => {
948                debug!("Deezer: get_url failed: {e}");
949                token_tracker.invalidate_token(arl_index).await;
950                return None;
951            }
952        };
953        if json
954            .get("data")
955            .and_then(|d| d.get(0))
956            .and_then(|d| d.get("errors"))
957            .and_then(|e| e.as_array())
958            .is_some_and(|e| !e.is_empty())
959        {
960            debug!("Deezer: get_url returned errors");
961            token_tracker.invalidate_token(arl_index).await;
962            return None;
963        }
964        let cdn_url = json
965            .get("data")?
966            .get(0)?
967            .get("media")?
968            .get(0)?
969            .get("sources")?
970            .get(0)?
971            .get("url")?
972            .as_str()?
973            .to_owned();
974        Some(ResolvedUrl {
975            cdn_url,
976            track_id: effective_track_id.to_owned(),
977            arl_index,
978        })
979    }
980    pub(super) async fn verify_track_resolvable(
981        client: &Arc<reqwest::Client>,
982        track_id: &str,
983        token_tracker: &DeezerTokenTracker,
984    ) -> Option<String> {
985        let tokens = token_tracker.get_token().await?;
986        let song_url = format!(
987            "https://www.deezer.com/ajax/gw-light.php?method=song.getData&input=3&api_version=1.0&api_token={}",
988            tokens.api_token
989        );
990        let json: serde_json::Value = client
991            .post(&song_url)
992            .header(
993                "Cookie",
994                format!(
995                    "sid={}; dzr_uniq_id={}",
996                    tokens.session_id, tokens.dzr_uniq_id
997                ),
998            )
999            .json(&serde_json::json!({ "sng_id": track_id }))
1000            .send()
1001            .await
1002            .ok()?
1003            .json()
1004            .await
1005            .ok()?;
1006        if json
1007            .get("error")
1008            .and_then(|v| v.as_array())
1009            .is_some_and(|e| !e.is_empty())
1010        {
1011            token_tracker.invalidate_token(tokens.arl_index).await;
1012            return None;
1013        }
1014        let mut results = match json.get("results") {
1015            Some(r) => r.clone(),
1016            None => {
1017                token_tracker.invalidate_token(tokens.arl_index).await;
1018                return None;
1019            }
1020        };
1021        let rights = results.get("RIGHTS");
1022        if is_rights_empty(rights)
1023            && let Some(fallback) = results.get("FALLBACK")
1024            && !fallback
1025                .get("TRACK_TOKEN")
1026                .map(|v| v.is_null())
1027                .unwrap_or(true)
1028        {
1029            let has_id = fallback.get("SNG_ID").and_then(|v| {
1030                v.as_str()
1031                    .map(|s| s.to_owned())
1032                    .or_else(|| v.as_i64().map(|n| n.to_string()))
1033            });
1034            if has_id.is_some() {
1035                results = fallback.clone();
1036            } else {
1037                warn!(
1038                    "Deezer: track {track_id} FALLBACK SNG_ID has unexpected format: {:?}",
1039                    fallback.get("SNG_ID")
1040                );
1041            }
1042        }
1043        let track_token = results
1044            .get("TRACK_TOKEN")
1045            .and_then(|v| v.as_str())?
1046            .to_owned();
1047        let media_json: serde_json::Value = client
1048            .post("https://media.deezer.com/v1/get_url")
1049            .json(&serde_json::json!({
1050                "license_token": tokens.license_token,
1051                "media": [{ "type": "FULL", "formats": [
1052                    { "cipher": "BF_CBC_STRIPE", "format": "MP3_128" }
1053                ]}],
1054                "track_tokens": [track_token]
1055            }))
1056            .send()
1057            .await
1058            .ok()?
1059            .json()
1060            .await
1061            .ok()?;
1062        if media_json
1063            .get("data")
1064            .and_then(|d| d.get(0))
1065            .and_then(|d| d.get("errors"))
1066            .and_then(|e| e.as_array())
1067            .is_some_and(|e| !e.is_empty())
1068        {
1069            token_tracker.invalidate_token(tokens.arl_index).await;
1070            return None;
1071        }
1072        media_json
1073            .get("data")?
1074            .get(0)?
1075            .get("media")?
1076            .get(0)?
1077            .get("sources")?
1078            .get(0)?
1079            .get("url")?
1080            .as_str()
1081            .map(|s| s.to_owned())
1082    }
1083    fn is_rights_empty(rights: Option<&serde_json::Value>) -> bool {
1084        rights
1085            .map(|v| {
1086                v.as_array()
1087                    .map(|a| a.is_empty())
1088                    .or_else(|| v.as_object().map(|o| o.is_empty()))
1089                    .unwrap_or(true)
1090            })
1091            .unwrap_or(true)
1092    }
1093}
1094pub mod reader {
1095    pub mod crypt {
1096        use cbc::cipher::{BlockDecryptMut, KeyIvInit};
1097        use md5::{Digest, Md5};
1098        use tracing::warn;
1099        type BlowfishCbc = cbc::Decryptor<blowfish::Blowfish>;
1100        pub const CHUNK_SIZE: usize = 2048;
1101        pub struct DeezerCrypt {
1102            key: [u8; 16],
1103        }
1104        impl DeezerCrypt {
1105            pub fn new(track_id: &str, master_key: &str) -> Self {
1106                let hash = Md5::digest(track_id.as_bytes());
1107                let hash_hex = hex::encode(hash);
1108                let hash_bytes = hash_hex.as_bytes();
1109                let master_bytes = master_key.as_bytes();
1110                let mut key = [0u8; 16];
1111                for i in 0..16 {
1112                    key[i] = hash_bytes[i] ^ hash_bytes[i + 16] ^ master_bytes[i];
1113                }
1114                Self { key }
1115            }
1116            pub fn decrypt_chunk(&self, chunk_index: u64, chunk: &[u8], dest: &mut Vec<u8>) {
1117                if chunk_index.is_multiple_of(3) {
1118                    let iv = [0, 1, 2, 3, 4, 5, 6, 7];
1119                    let mut buffer = [0u8; CHUNK_SIZE];
1120                    let len = std::cmp::min(chunk.len(), CHUNK_SIZE);
1121                    buffer[..len].copy_from_slice(&chunk[..len]);
1122                    if let Ok(cipher) = BlowfishCbc::new_from_slices(&self.key, &iv) {
1123                        if let Ok(decrypted) = cipher
1124                            .decrypt_padded_mut::<cbc::cipher::block_padding::NoPadding>(
1125                                &mut buffer,
1126                            )
1127                        {
1128                            dest.extend_from_slice(decrypted);
1129                            return;
1130                        } else {
1131                            warn!(
1132                                "Blowfish decryption failed for chunk {}, falling back to raw",
1133                                chunk_index
1134                            );
1135                        }
1136                    }
1137                }
1138                dest.extend_from_slice(chunk);
1139            }
1140        }
1141    }
1142    pub mod remote_reader {
1143        use crate::{
1144            audio::source::{AudioSource, HttpSource, create_client},
1145            common::types::AnyResult,
1146        };
1147        use std::io::{Read, Seek, SeekFrom};
1148        use symphonia::core::io::MediaSource;
1149        pub struct DeezerRemoteReader {
1150            inner: HttpSource,
1151        }
1152        const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36";
1153        impl DeezerRemoteReader {
1154            pub async fn new(
1155                url: &str,
1156                local_addr: Option<std::net::IpAddr>,
1157                proxy: Option<crate::config::HttpProxyConfig>,
1158            ) -> AnyResult<Self> {
1159                let client = create_client(USER_AGENT.to_owned(), local_addr, proxy, None)?;
1160                let inner = HttpSource::new(client, url).await?;
1161                Ok(Self { inner })
1162            }
1163        }
1164        impl Read for DeezerRemoteReader {
1165            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1166                self.inner.read(buf)
1167            }
1168        }
1169        impl Seek for DeezerRemoteReader {
1170            fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
1171                self.inner.seek(pos)
1172            }
1173        }
1174        impl MediaSource for DeezerRemoteReader {
1175            fn is_seekable(&self) -> bool {
1176                self.inner.is_seekable()
1177            }
1178            fn byte_len(&self) -> Option<u64> {
1179                self.inner.byte_len()
1180            }
1181        }
1182        impl DeezerRemoteReader {
1183            pub fn content_type(&self) -> Option<String> {
1184                self.inner.content_type()
1185            }
1186        }
1187    }
1188    use self::{
1189        crypt::{CHUNK_SIZE, DeezerCrypt},
1190        remote_reader::DeezerRemoteReader,
1191    };
1192    use crate::common::types::AnyResult;
1193    use std::io::{Read, Seek, SeekFrom};
1194    use symphonia::core::io::MediaSource;
1195    use tracing::debug;
1196    pub struct DeezerReader {
1197        source: DeezerRemoteReader,
1198        crypt: DeezerCrypt,
1199        pos: u64,
1200        raw_buf: Vec<u8>,
1201        ready_buf: Vec<u8>,
1202        skip_pending: usize,
1203    }
1204    impl DeezerReader {
1205        pub async fn new(
1206            url: &str,
1207            track_id: &str,
1208            master_key: &str,
1209            local_addr: Option<std::net::IpAddr>,
1210            proxy: Option<crate::config::HttpProxyConfig>,
1211        ) -> AnyResult<Self> {
1212            debug!("Initializing DeezerReader for track {}", track_id);
1213            let source = DeezerRemoteReader::new(url, local_addr, proxy).await?;
1214            let crypt = DeezerCrypt::new(track_id, master_key);
1215            Ok(Self {
1216                source,
1217                crypt,
1218                pos: 0,
1219                raw_buf: Vec::with_capacity(CHUNK_SIZE * 2),
1220                ready_buf: Vec::with_capacity(CHUNK_SIZE * 2),
1221                skip_pending: 0,
1222            })
1223        }
1224    }
1225    impl Read for DeezerReader {
1226        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1227            loop {
1228                if self.skip_pending > 0 && !self.ready_buf.is_empty() {
1229                    let to_skip = std::cmp::min(self.skip_pending, self.ready_buf.len());
1230                    self.ready_buf.drain(..to_skip);
1231                    self.skip_pending -= to_skip;
1232                }
1233                if self.skip_pending == 0 && !self.ready_buf.is_empty() {
1234                    let n = std::cmp::min(buf.len(), self.ready_buf.len());
1235                    buf[..n].copy_from_slice(&self.ready_buf[..n]);
1236                    self.ready_buf.drain(..n);
1237                    return Ok(n);
1238                }
1239                let mut tmp = [0u8; CHUNK_SIZE];
1240                let n = self.source.read(&mut tmp)?;
1241                if n == 0 {
1242                    if self.raw_buf.is_empty() {
1243                        return Ok(0);
1244                    }
1245                    let leftovers = self.raw_buf.clone();
1246                    let chunk_idx = self.pos / CHUNK_SIZE as u64;
1247                    self.crypt
1248                        .decrypt_chunk(chunk_idx, &leftovers, &mut self.ready_buf);
1249                    self.pos += leftovers.len() as u64;
1250                    self.raw_buf.clear();
1251                    continue;
1252                }
1253                self.raw_buf.extend_from_slice(&tmp[..n]);
1254                while self.raw_buf.len() >= CHUNK_SIZE {
1255                    let chunk: Vec<u8> = self.raw_buf.drain(..CHUNK_SIZE).collect();
1256                    let chunk_idx = self.pos / CHUNK_SIZE as u64;
1257                    self.crypt
1258                        .decrypt_chunk(chunk_idx, &chunk, &mut self.ready_buf);
1259                    self.pos += CHUNK_SIZE as u64;
1260                }
1261            }
1262        }
1263    }
1264    impl Seek for DeezerReader {
1265        fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
1266            let target = match pos {
1267                SeekFrom::Start(p) => p,
1268                SeekFrom::Current(0) => {
1269                    let buffered = self.ready_buf.len() as u64 + self.raw_buf.len() as u64;
1270                    return Ok(self.pos.saturating_sub(buffered));
1271                }
1272                _ => {
1273                    return Err(std::io::Error::new(
1274                        std::io::ErrorKind::Unsupported,
1275                        "Only SeekFrom::Start is supported",
1276                    ));
1277                }
1278            };
1279            let aligned_pos = (target / CHUNK_SIZE as u64) * CHUNK_SIZE as u64;
1280            let skip = (target - aligned_pos) as usize;
1281            let new_pos = self.source.seek(SeekFrom::Start(aligned_pos))?;
1282            self.pos = new_pos;
1283            self.raw_buf.clear();
1284            self.ready_buf.clear();
1285            self.skip_pending = skip;
1286            Ok(target)
1287        }
1288    }
1289    impl MediaSource for DeezerReader {
1290        fn is_seekable(&self) -> bool {
1291            self.source.is_seekable()
1292        }
1293        fn byte_len(&self) -> Option<u64> {
1294            self.source.byte_len()
1295        }
1296    }
1297}
1298use crate::{
1299    protocol::tracks::LoadResult,
1300    sources::{SourcePlugin, playable_track::BoxedTrack},
1301};
1302use async_trait::async_trait;
1303use regex::Regex;
1304use std::sync::{Arc, OnceLock};
1305use token::DeezerTokenTracker;
1306use track::DeezerTrack;
1307const PUBLIC_API_BASE: &str = "https://api.deezer.com";
1308const PRIVATE_API_BASE: &str = "https://www.deezer.com/ajax/gw-light.php";
1309pub(crate) const REC_ARTIST_PREFIX: &str = "artist=";
1310pub(crate) const REC_TRACK_PREFIX: &str = "track=";
1311fn url_regex() -> &'static Regex {
1312    static REGEX: OnceLock<Regex> = OnceLock::new();
1313    REGEX.get_or_init(|| {
1314        Regex::new(r"https?://(?:www\.)?deezer\.com/(?:[a-z]+(?:-[a-z]+)?/)?(?<type>track|album|playlist|artist)/(?<id>\d+)").unwrap()
1315    })
1316}
1317fn share_url_regex() -> &'static Regex {
1318    static REGEX: OnceLock<Regex> = OnceLock::new();
1319    REGEX.get_or_init(|| {
1320        Regex::new(r"https?://(?:deezer\.page\.link|link\.deezer\.com)/\S*").unwrap()
1321    })
1322}
1323pub struct DeezerSource {
1324    client: Arc<reqwest::Client>,
1325    config: crate::config::DeezerConfig,
1326    pub token_tracker: Arc<DeezerTokenTracker>,
1327}
1328const DECRYPTION_KEY_HASH: [u8; 32] = [
1329    52, 76, 41, 138, 120, 133, 48, 72, 198, 74, 16, 75, 82, 101, 186, 223, 15, 190, 111, 218, 176,
1330    71, 103, 11, 181, 136, 155, 247, 66, 203, 218, 240,
1331];
1332impl DeezerSource {
1333    pub fn new(
1334        config: crate::config::DeezerConfig,
1335        client: Arc<reqwest::Client>,
1336    ) -> Result<Self, String> {
1337        let mut arls = config.arls.clone().unwrap_or_default();
1338        arls.retain(|s| !s.is_empty());
1339        arls.sort();
1340        arls.dedup();
1341        if arls.is_empty() {
1342            return Err("Deezer arls must be set".to_owned());
1343        }
1344        if let Some(ref key) = config.master_decryption_key {
1345            use sha2::{Digest, Sha256};
1346            let mut hasher = Sha256::new();
1347            hasher.update(key.as_bytes());
1348            if hasher.finalize().as_slice() != DECRYPTION_KEY_HASH {
1349                tracing::warn!("Deezer master decryption key is invalid, playback may not work!");
1350            }
1351        }
1352        let token_tracker = Arc::new(DeezerTokenTracker::new(client.clone(), arls));
1353        Ok(Self {
1354            client,
1355            config,
1356            token_tracker,
1357        })
1358    }
1359    async fn resolve_share_url(&self, identifier: &str) -> Option<String> {
1360        let client = reqwest::Client::builder()
1361            .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6094.0 Safari/537.36")
1362            .redirect(reqwest::redirect::Policy::none())
1363            .build()
1364            .ok()?;
1365        let res = client.get(identifier).send().await.ok()?;
1366        if !res.status().is_redirection() {
1367            return None;
1368        }
1369        let loc = res.headers().get("location")?.to_str().ok()?;
1370        let mut url = loc.to_owned();
1371        if let Some(pos) = url.find("dest=") {
1372            let dest = &url[pos + 5..];
1373            let end = dest.find('&').unwrap_or(dest.len());
1374            if let Ok(decoded) = urlencoding::decode(&dest[..end]) {
1375                url = decoded.into_owned();
1376            }
1377        }
1378        if let Some(pos) = url.find('?') {
1379            url.truncate(pos);
1380        }
1381        if url.ends_with("/404") {
1382            return None;
1383        }
1384        Some(url)
1385    }
1386}
1387#[async_trait]
1388impl SourcePlugin for DeezerSource {
1389    fn name(&self) -> &str {
1390        "deezer"
1391    }
1392    fn can_handle(&self, identifier: &str) -> bool {
1393        self.search_prefixes()
1394            .iter()
1395            .any(|p| identifier.starts_with(p))
1396            || self
1397                .isrc_prefixes()
1398                .iter()
1399                .any(|p| identifier.starts_with(p))
1400            || self
1401                .rec_prefixes()
1402                .iter()
1403                .any(|p| identifier.starts_with(p))
1404            || share_url_regex().is_match(identifier)
1405            || url_regex().is_match(identifier)
1406    }
1407    fn search_prefixes(&self) -> Vec<&str> {
1408        vec!["dzsearch:"]
1409    }
1410    fn isrc_prefixes(&self) -> Vec<&str> {
1411        vec!["dzisrc:"]
1412    }
1413    fn rec_prefixes(&self) -> Vec<&str> {
1414        vec!["dzrec:"]
1415    }
1416    async fn load(
1417        &self,
1418        identifier: &str,
1419        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
1420    ) -> LoadResult {
1421        for prefix in self.search_prefixes() {
1422            if let Some(query) = identifier.strip_prefix(prefix) {
1423                return self.search(query).await;
1424            }
1425        }
1426        for prefix in self.isrc_prefixes() {
1427            if let Some(isrc) = identifier.strip_prefix(prefix) {
1428                if let Some(track) = self.get_track_by_isrc(isrc).await {
1429                    return LoadResult::Track(track);
1430                }
1431                return LoadResult::Empty {};
1432            }
1433        }
1434        for prefix in self.rec_prefixes() {
1435            if let Some(query) = identifier.strip_prefix(prefix) {
1436                return self.get_recommendations(query).await;
1437            }
1438        }
1439        if share_url_regex().is_match(identifier) {
1440            if let Some(resolved) = self.resolve_share_url(identifier).await {
1441                return self.load(&resolved, routeplanner).await;
1442            }
1443            return LoadResult::Empty {};
1444        }
1445        if let Some(caps) = url_regex().captures(identifier) {
1446            let type_ = caps.name("type").map(|m| m.as_str()).unwrap_or("");
1447            let id = caps.name("id").map(|m| m.as_str()).unwrap_or("");
1448            return match type_ {
1449                "track" => {
1450                    if let Some(json) = self.get_json_public(&format!("track/{id}")).await
1451                        && let Some(track) = self.parse_track(&json)
1452                    {
1453                        return LoadResult::Track(track);
1454                    }
1455                    LoadResult::Empty {}
1456                }
1457                "album" => self.get_album(id).await,
1458                "playlist" => self.get_playlist(id).await,
1459                "artist" => self.get_artist(id).await,
1460                _ => LoadResult::Empty {},
1461            };
1462        }
1463        LoadResult::Empty {}
1464    }
1465    async fn get_track(
1466        &self,
1467        identifier: &str,
1468        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
1469    ) -> Option<BoxedTrack> {
1470        let track_id = if let Some(caps) = url_regex().captures(identifier) {
1471            caps.name("id").map(|m| m.as_str())?.to_owned()
1472        } else {
1473            identifier.to_owned()
1474        };
1475        let resolved =
1476            track::verify_track_resolvable(&self.client, &track_id, &self.token_tracker).await;
1477        if resolved.is_none() {
1478            tracing::warn!("Deezer: no stream URL for track {track_id}, falling back to mirrors");
1479            return None;
1480        }
1481        Some(Arc::new(DeezerTrack {
1482            client: self.client.clone(),
1483            track_id,
1484            token_tracker: self.token_tracker.clone(),
1485            master_key: self
1486                .config
1487                .master_decryption_key
1488                .clone()
1489                .unwrap_or_default(),
1490            local_addr: routeplanner.and_then(|rp| rp.get_address()),
1491            proxy: self.config.proxy.clone(),
1492        }))
1493    }
1494    async fn load_search(
1495        &self,
1496        query: &str,
1497        types: &[String],
1498        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
1499    ) -> Option<crate::protocol::tracks::SearchResult> {
1500        let mut q = query;
1501        for prefix in self.search_prefixes() {
1502            if let Some(stripped) = query.strip_prefix(prefix) {
1503                q = stripped;
1504                break;
1505            }
1506        }
1507        self.get_autocomplete(q, types).await
1508    }
1509}