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}