Skip to main content

lavende_core/sources/
applemusic.rs

1pub mod helpers {
2    use super::{API_BASE, AppleMusicSource};
3    use serde_json::Value;
4    use tracing::{error, warn};
5    impl AppleMusicSource {
6        pub(crate) async fn api_request(&self, path: &str) -> Option<Value> {
7            let token = self.token_tracker.get_token().await?;
8            let origin = self.token_tracker.get_origin().await;
9            let url = if path.starts_with("http") {
10                path.to_owned()
11            } else {
12                format!("{}{}", API_BASE, path)
13            };
14            let mut req = self.client.get(&url).bearer_auth(token);
15            if let Some(o) = origin {
16                req = req.header("Origin", format!("https://{}", o));
17            }
18            let resp = match req.send().await {
19                Ok(r) => r,
20                Err(e) => {
21                    error!("Apple Music API request failed: {}", e);
22                    return None;
23                }
24            };
25            if !resp.status().is_success() {
26                warn!("Apple Music API returned {} for {}", resp.status(), url);
27                return None;
28            }
29            resp.json().await.ok()
30        }
31    }
32}
33pub mod metadata {
34    use super::AppleMusicSource;
35    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo};
36    use futures::future::join_all;
37    use serde_json::Value;
38    use std::sync::Arc;
39    use tokio::sync::Semaphore;
40    impl AppleMusicSource {
41        pub(crate) async fn resolve_track(&self, id: &str) -> LoadResult {
42            let path = format!("/catalog/{}/songs/{}", self.country_code, id);
43            let data = match self.api_request(&path).await {
44                Some(d) => d,
45                None => return LoadResult::Empty {},
46            };
47            if let Some(item) = data.pointer("/data/0")
48                && let Some(track) = self.build_track(item, None)
49            {
50                return LoadResult::Track(track);
51            }
52            LoadResult::Empty {}
53        }
54        pub(crate) async fn resolve_album(&self, id: &str) -> LoadResult {
55            self.resolve_collection(id, "album").await
56        }
57        pub(crate) async fn resolve_playlist(&self, id: &str) -> LoadResult {
58            self.resolve_collection(id, "playlist").await
59        }
60        async fn resolve_collection(&self, id: &str, kind: &str) -> LoadResult {
61            let plural = match kind {
62                "album" => "albums",
63                "playlist" => "playlists",
64                _ => return LoadResult::Empty {},
65            };
66            let path = format!("/catalog/{}/{}/{}", self.country_code, plural, id);
67            let data = match self.api_request(&path).await {
68                Some(d) => d,
69                None => return LoadResult::Empty {},
70            };
71            let collection = match data.pointer("/data/0") {
72                Some(c) => c,
73                None => return LoadResult::Empty {},
74            };
75            let attributes = match collection.get("attributes") {
76                Some(a) => a,
77                None => return LoadResult::Empty {},
78            };
79            let name = attributes
80                .get("name")
81                .and_then(|v| v.as_str())
82                .unwrap_or("Unknown")
83                .to_owned();
84            let artwork = attributes
85                .pointer("/artwork/url")
86                .and_then(|v| v.as_str())
87                .map(|s| s.replace("{w}", "1000").replace("{h}", "1000"));
88            let tracks_rel = match collection
89                .get("relationships")
90                .and_then(|r| r.get("tracks"))
91            {
92                Some(t) => t,
93                None => return LoadResult::Empty {},
94            };
95            let mut all_items = tracks_rel
96                .get("data")
97                .and_then(|v| v.as_array())
98                .cloned()
99                .unwrap_or_default();
100            let next_url = tracks_rel.get("next").and_then(|v| v.as_str());
101            let (load_limit, concurrency) = if kind == "album" {
102                (self.album_load_limit, self.album_page_load_concurrency)
103            } else {
104                (
105                    self.playlist_load_limit,
106                    self.playlist_page_load_concurrency,
107                )
108            };
109            if next_url.is_some() && (load_limit == 0 || load_limit > 1) {
110                let next_url_owned = next_url.map(|s| s.to_owned());
111                let extra = self
112                    .fetch_paginated_tracks(next_url_owned, load_limit, concurrency)
113                    .await;
114                all_items.extend(extra);
115            }
116            let mut tracks = Vec::new();
117            for item in all_items {
118                if let Some(track) = self.build_track(&item, artwork.clone()) {
119                    tracks.push(track);
120                }
121            }
122            if tracks.is_empty() {
123                return LoadResult::Empty {};
124            }
125            let author = if kind == "album" {
126                attributes.get("artistName").and_then(|v| v.as_str())
127            } else {
128                attributes.get("curatorName").and_then(|v| v.as_str())
129            };
130            LoadResult::Playlist(PlaylistData {
131                info: PlaylistInfo {
132                    name,
133                    selected_track: -1,
134                },
135                plugin_info: serde_json::json!({
136                    "type": kind,
137                    "url": attributes.get("url").and_then(|v| v.as_str()),
138                    "artworkUrl": artwork,
139                    "author": author,
140                    "totalTracks": attributes.get("trackCount").and_then(|v| v.as_u64()).unwrap_or(tracks.len() as u64)
141                }),
142                tracks,
143            })
144        }
145        pub(crate) async fn resolve_artist(&self, id: &str) -> LoadResult {
146            let path = format!(
147                "/catalog/{}/artists/{}/view/top-songs",
148                self.country_code, id
149            );
150            let data = match self.api_request(&path).await {
151                Some(d) => d,
152                None => return LoadResult::Empty {},
153            };
154            let tracks_data = data.pointer("/data").and_then(|v| v.as_array());
155            let artist_path = format!("/catalog/{}/artists/{}", self.country_code, id);
156            let artist_data = self.api_request(&artist_path).await;
157            let (artist_name, artwork) = if let Some(ad) = artist_data {
158                let name = ad
159                    .pointer("/data/0/attributes/name")
160                    .and_then(|v| v.as_str())
161                    .unwrap_or("Artist")
162                    .to_owned();
163                let art = ad
164                    .pointer("/data/0/attributes/artwork/url")
165                    .and_then(|v| v.as_str())
166                    .map(|s| s.replace("{w}", "1000").replace("{h}", "1000"));
167                (name, art)
168            } else {
169                ("Artist".to_owned(), None)
170            };
171            let mut tracks = Vec::new();
172            if let Some(items) = tracks_data {
173                for item in items {
174                    if let Some(track) = self.build_track(item, artwork.clone()) {
175                        tracks.push(track);
176                    }
177                }
178            }
179            if tracks.is_empty() {
180                return LoadResult::Empty {};
181            }
182            LoadResult::Playlist(PlaylistData {
183                info: PlaylistInfo {
184                    name: format!("{}'s Top Tracks", artist_name),
185                    selected_track: -1,
186                },
187                plugin_info: serde_json::json!({
188                    "type": "artist",
189                    "url": format!("https://music.apple.com/artist/{}", id),
190                    "artworkUrl": artwork,
191                    "author": artist_name,
192                    "totalTracks": tracks.len()
193                }),
194                tracks,
195            })
196        }
197        async fn fetch_paginated_tracks(
198            &self,
199            next_url: Option<String>,
200            load_limit: usize,
201            concurrency: usize,
202        ) -> Vec<Value> {
203            let initial_next = match next_url {
204                Some(u) => u,
205                None => return Vec::new(),
206            };
207            if initial_next.contains("offset=") {
208                let base_url = initial_next
209                    .split("offset=")
210                    .next()
211                    .unwrap_or(&initial_next)
212                    .to_owned();
213                let offset: usize = initial_next
214                    .split("offset=")
215                    .nth(1)
216                    .and_then(|s| s.split('&').next())
217                    .and_then(|s| s.parse().ok())
218                    .unwrap_or(100);
219                let mut all_items = Vec::new();
220                let mut current_offset = offset;
221                let mut limit_reached = false;
222                let mut pages_fetched = 1;
223                while !limit_reached {
224                    let mut futs = Vec::new();
225                    let semaphore = Arc::new(Semaphore::new(concurrency));
226                    for _ in 0..concurrency {
227                        if load_limit > 0 && pages_fetched >= load_limit {
228                            limit_reached = true;
229                            break;
230                        }
231                        let url = format!("{}offset={}", base_url, current_offset);
232                        let sem = semaphore.clone();
233                        futs.push(async move {
234                            let _permit = sem.acquire().await.ok();
235                            self.api_request(&url).await
236                        });
237                        current_offset += 100;
238                        pages_fetched += 1;
239                    }
240                    if futs.is_empty() {
241                        break;
242                    }
243                    let results = join_all(futs).await;
244                    let mut added_on_this_step = 0;
245                    for res in results {
246                        if let Some(data) = res {
247                            if let Some(items) = data.get("data").and_then(|v| v.as_array()) {
248                                all_items.extend(items.iter().cloned());
249                                added_on_this_step += items.len();
250                                if items.len() < 100 {
251                                    limit_reached = true;
252                                }
253                            } else {
254                                limit_reached = true;
255                            }
256                        } else {
257                            limit_reached = true;
258                        }
259                    }
260                    if added_on_this_step == 0 {
261                        break;
262                    }
263                }
264                return all_items;
265            }
266            let mut next = Some(initial_next);
267            let mut all_items = Vec::new();
268            let mut pages_fetched = 1;
269            while let Some(url) = next {
270                if load_limit > 0 && pages_fetched >= load_limit {
271                    break;
272                }
273                let data = match self.api_request(&url).await {
274                    Some(d) => d,
275                    None => break,
276                };
277                if let Some(items) = data.get("data").and_then(|v| v.as_array()) {
278                    all_items.extend(items.iter().cloned());
279                }
280                next = data
281                    .get("next")
282                    .and_then(|v| v.as_str())
283                    .map(|s| s.to_owned());
284                pages_fetched += 1;
285            }
286            all_items
287        }
288    }
289}
290pub mod parser {
291    use super::AppleMusicSource;
292    use crate::protocol::tracks::{Track, TrackInfo};
293    use serde_json::{Value, json};
294    impl AppleMusicSource {
295        pub(crate) fn build_track(
296            &self,
297            item: &Value,
298            artwork_override: Option<String>,
299        ) -> Option<Track> {
300            let attributes = item.get("attributes")?;
301            let id = item.get("id")?.as_str()?.to_owned();
302            let title = attributes
303                .get("name")
304                .and_then(|v| v.as_str())
305                .unwrap_or("Unknown Title")
306                .to_owned();
307            let author = attributes
308                .get("artistName")
309                .and_then(|v| v.as_str())
310                .unwrap_or("Unknown Artist")
311                .to_owned();
312            let length = attributes
313                .get("durationInMillis")
314                .and_then(|v| v.as_u64())
315                .unwrap_or(0);
316            let isrc = attributes
317                .get("isrc")
318                .and_then(|v| v.as_str())
319                .map(|s| s.to_owned());
320            let artwork_url = artwork_override.or_else(|| {
321                attributes
322                    .get("artwork")
323                    .and_then(|a| a.get("url"))
324                    .and_then(|v| v.as_str())
325                    .filter(|s| !s.is_empty())
326                    .map(|s| s.replace("{w}", "1000").replace("{h}", "1000"))
327            });
328            let url = attributes
329                .get("url")
330                .and_then(|v| v.as_str())
331                .filter(|s| !s.is_empty())
332                .map(|s| s.to_owned());
333            let mut track = Track::new(TrackInfo {
334                title,
335                author,
336                length,
337                identifier: id,
338                is_stream: false,
339                uri: url,
340                artwork_url,
341                isrc,
342                source_name: "applemusic".to_owned(),
343                is_seekable: true,
344                position: 0,
345            });
346            let album_name = attributes.get("albumName").and_then(|v| v.as_str());
347            let artist_url = attributes.get("artistUrl").and_then(|v| v.as_str());
348            let preview_url = attributes
349                .pointer("/previews/0/url")
350                .and_then(|v| v.as_str());
351            let album_url = track
352                .info
353                .uri
354                .as_ref()
355                .and_then(|u| u.split('?').next().map(|s| s.to_owned()));
356            track.plugin_info = json!({
357                "albumName": album_name,
358                "albumUrl": album_url,
359                "artistUrl": artist_url,
360                "artistArtworkUrl": null,
361                "previewUrl": preview_url,
362                "isPreview": false
363            });
364            Some(track)
365        }
366    }
367}
368pub mod search {
369    use super::{API_BASE, AppleMusicSource};
370    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo, SearchResult};
371    use serde_json::Value;
372    use std::collections::HashSet;
373    impl AppleMusicSource {
374        pub(crate) async fn search(&self, query: &str) -> LoadResult {
375            let encoded_query = urlencoding::encode(query);
376            let path = format!(
377                "/catalog/{}/search?term={}&limit=10&types=songs",
378                self.country_code, encoded_query
379            );
380            let data = match self.api_request(&path).await {
381                Some(d) => d,
382                None => return LoadResult::Empty {},
383            };
384            let songs = data
385                .pointer("/results/songs/data")
386                .and_then(|v| v.as_array());
387            let mut tracks = Vec::new();
388            if let Some(items) = songs {
389                for item in items {
390                    if let Some(track) = self.build_track(item, None) {
391                        tracks.push(track);
392                    }
393                }
394            }
395            if tracks.is_empty() {
396                LoadResult::Empty {}
397            } else {
398                LoadResult::Search(tracks)
399            }
400        }
401        pub(crate) async fn get_search_suggestions(
402            &self,
403            query: &str,
404            types: &[String],
405        ) -> Option<SearchResult> {
406            let mut kinds = HashSet::new();
407            let mut am_types = Vec::new();
408            let all_types = types.is_empty();
409            if all_types
410                || types.contains(&"track".to_owned())
411                || types.contains(&"album".to_owned())
412                || types.contains(&"artist".to_owned())
413                || types.contains(&"playlist".to_owned())
414            {
415                kinds.insert("topResults");
416            }
417            if types.contains(&"text".to_owned()) {
418                kinds.insert("terms");
419            }
420            if all_types || types.contains(&"track".to_owned()) {
421                am_types.push("songs");
422            }
423            if all_types || types.contains(&"album".to_owned()) {
424                am_types.push("albums");
425            }
426            if all_types || types.contains(&"artist".to_owned()) {
427                am_types.push("artists");
428            }
429            if all_types || types.contains(&"playlist".to_owned()) {
430                am_types.push("playlists");
431            }
432            let kinds_str = kinds.into_iter().collect::<Vec<_>>().join(",");
433            let types_str = am_types.join(",");
434            let mut params = vec![
435                ("term", query.to_owned()),
436                ("extend", "artistUrl".to_owned()),
437                ("kinds", kinds_str),
438            ];
439            if !types_str.is_empty() {
440                params.push(("types", types_str));
441            }
442            let path = format!("/catalog/{}/search/suggestions", self.country_code);
443            let mut url = format!("{}{}", API_BASE, path);
444            if !params.is_empty() {
445                url.push('?');
446                for (i, (k, v)) in params.iter().enumerate() {
447                    if i > 0 {
448                        url.push('&');
449                    }
450                    url.push_str(k);
451                    url.push('=');
452                    url.push_str(&urlencoding::encode(v));
453                }
454            }
455            let json = self.api_request(&url).await?;
456            let suggestions = json.pointer("/results/suggestions")?.as_array()?;
457            let mut tracks = Vec::new();
458            let mut albums = Vec::new();
459            let mut artists = Vec::new();
460            let mut playlists = Vec::new();
461            for suggestion in suggestions {
462                let kind = suggestion
463                    .get("kind")
464                    .and_then(|v| v.as_str())
465                    .unwrap_or("");
466                if kind == "terms" {
467                    continue;
468                }
469                let content = match suggestion.get("content") {
470                    Some(c) => c,
471                    None => continue,
472                };
473                let type_ = content.get("type").and_then(|v| v.as_str()).unwrap_or("");
474                match type_ {
475                    "songs" => {
476                        if let Some(track) = self.build_track(content, None) {
477                            tracks.push(track);
478                        }
479                    }
480                    "albums" => {
481                        if let Some(album) = self.build_collection(content, "album") {
482                            albums.push(album);
483                        }
484                    }
485                    "artists" => {
486                        if let Some(artist) = self.build_collection(content, "artist") {
487                            artists.push(artist);
488                        }
489                    }
490                    "playlists" => {
491                        if let Some(playlist) = self.build_collection(content, "playlist") {
492                            playlists.push(playlist);
493                        }
494                    }
495                    _ => {}
496                }
497            }
498            Some(SearchResult {
499                tracks,
500                albums,
501                artists,
502                playlists,
503                texts: Vec::new(),
504                plugin: serde_json::json!({}),
505            })
506        }
507        fn build_collection(&self, content: &Value, kind: &str) -> Option<PlaylistData> {
508            let attributes = content.get("attributes")?;
509            let url = attributes.get("url").and_then(|v| v.as_str()).unwrap_or("");
510            let name = attributes
511                .get("name")
512                .and_then(|v| v.as_str())
513                .unwrap_or("Unknown");
514            let artwork = attributes
515                .pointer("/artwork/url")
516                .and_then(|v| v.as_str())
517                .map(|s| s.replace("{w}", "500").replace("{h}", "500"));
518            let (author, track_count, display_name) = match kind {
519                "album" => (
520                    attributes
521                        .get("artistName")
522                        .and_then(|v| v.as_str())
523                        .unwrap_or("Unknown Artist")
524                        .to_owned(),
525                    attributes
526                        .get("trackCount")
527                        .and_then(|v| v.as_u64())
528                        .unwrap_or(0),
529                    name.to_owned(),
530                ),
531                "artist" => (name.to_owned(), 0, format!("{}'s Top Tracks", name)),
532                "playlist" => (
533                    attributes
534                        .get("curatorName")
535                        .and_then(|v| v.as_str())
536                        .unwrap_or("Apple Music")
537                        .to_owned(),
538                    attributes
539                        .get("trackCount")
540                        .and_then(|v| v.as_u64())
541                        .unwrap_or(0),
542                    name.to_owned(),
543                ),
544                _ => return None,
545            };
546            Some(PlaylistData {
547                info: PlaylistInfo {
548                    name: display_name,
549                    selected_track: -1,
550                },
551                plugin_info: serde_json::json!({
552                    "type": kind,
553                    "url": url,
554                    "author": author,
555                    "artworkUrl": artwork,
556                    "totalTracks": track_count
557                }),
558                tracks: Vec::new(),
559            })
560        }
561    }
562}
563pub mod token {
564    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
565    use regex::Regex;
566    use serde_json::Value;
567    use std::{
568        sync::{Arc, OnceLock},
569        time::{Duration, SystemTime, UNIX_EPOCH},
570    };
571    use tokio::sync::RwLock;
572    use tracing::{error, info, warn};
573    #[derive(Clone, Debug)]
574    pub struct AppleMusicToken {
575        pub access_token: String,
576        pub origin: Option<String>,
577        pub expiry_ms: u64,
578    }
579    pub struct AppleMusicTokenTracker {
580        token: RwLock<Option<AppleMusicToken>>,
581        client: Arc<reqwest::Client>,
582    }
583    impl AppleMusicTokenTracker {
584        pub fn new(client: Arc<reqwest::Client>) -> Self {
585            Self {
586                token: RwLock::new(None),
587                client,
588            }
589        }
590        pub async fn get_token(&self) -> Option<String> {
591            {
592                let lock = self.token.read().await;
593                if let Some(token) = &*lock
594                    && self.is_valid(token)
595                {
596                    return Some(token.access_token.clone());
597                }
598            }
599            self.refresh_token().await
600        }
601        pub async fn get_origin(&self) -> Option<String> {
602            let lock = self.token.read().await;
603            lock.as_ref().and_then(|t| t.origin.clone())
604        }
605        fn is_valid(&self, token: &AppleMusicToken) -> bool {
606            let now = SystemTime::now()
607                .duration_since(UNIX_EPOCH)
608                .unwrap_or_default()
609                .as_millis() as u64;
610            token.expiry_ms > now + 10_000
611        }
612        async fn refresh_token(&self) -> Option<String> {
613            info!("Fetching new Apple Music API token...");
614            let browse_url = "https://music.apple.com";
615            let resp = match self.client.get(browse_url).send().await {
616                Ok(r) => r,
617                Err(e) => {
618                    error!("Failed to fetch Apple Music root page: {}", e);
619                    return None;
620                }
621            };
622            if !resp.status().is_success() {
623                error!("Apple Music root page returned status: {}", resp.status());
624                return None;
625            }
626            let html = resp.text().await.unwrap_or_default();
627            static SCRIPT_REGEX: OnceLock<Regex> = OnceLock::new();
628            static INDEX_REGEX: OnceLock<Regex> = OnceLock::new();
629            let script_re = SCRIPT_REGEX.get_or_init(|| {
630                Regex::new(
631                    r#"<script\s+type="module"\s+crossorigin\s+src="(/assets/index[^"]+\.js)""#,
632                )
633                .unwrap()
634            });
635            let script_path = match script_re.captures(&html) {
636                Some(caps) => caps.get(1).map(|m| m.as_str()),
637                None => {
638                    let index_re = INDEX_REGEX
639                        .get_or_init(|| Regex::new(r#"/assets/index[^"]+\.js"#).unwrap());
640                    index_re.find(&html).map(|m| m.as_str())
641                }
642            };
643            let script_path = match script_path {
644                Some(p) => p,
645                None => {
646                    error!("Could not find index JS in Apple Music HTML");
647                    return None;
648                }
649            };
650            let script_url = if script_path.starts_with("http") {
651                script_path.to_owned()
652            } else {
653                format!("https://music.apple.com{}", script_path)
654            };
655            let js_resp = match self.client.get(&script_url).send().await {
656                Ok(r) => r,
657                Err(e) => {
658                    error!("Failed to fetch Apple Music JS bundle: {}", e);
659                    return None;
660                }
661            };
662            let js_content = js_resp.text().await.unwrap_or_default();
663            static TOKEN_REGEX: OnceLock<Regex> = OnceLock::new();
664            let token_re =
665                TOKEN_REGEX.get_or_init(|| Regex::new(r#"(ey[\w-]+\.[\w-]+\.[\w-]+)"#).unwrap());
666            let token_str = match token_re.find(&js_content) {
667                Some(m) => m.as_str().to_owned(),
668                None => {
669                    error!("Could not find bearer token in Apple Music JS");
670                    return None;
671                }
672            };
673            let (origin, expiry_ms) = self.parse_jwt(&token_str).unwrap_or((None, 0));
674            let token = AppleMusicToken {
675                access_token: token_str.clone(),
676                origin,
677                expiry_ms,
678            };
679            let mut lock = self.token.write().await;
680            *lock = Some(token);
681            info!("Successfully refreshed Apple Music token");
682            Some(token_str)
683        }
684        fn parse_jwt(&self, token: &str) -> Option<(Option<String>, u64)> {
685            let parts: Vec<&str> = token.split('.').collect();
686            if parts.len() < 2 {
687                return None;
688            }
689            let payload_part = parts[1];
690            let decoded = URL_SAFE_NO_PAD.decode(payload_part).ok()?;
691            let json: Value = serde_json::from_slice(&decoded).ok()?;
692            let origin = json
693                .get("root_https_origin")
694                .and_then(|v| v.as_array())
695                .and_then(|a| a.first())
696                .and_then(|v| v.as_str())
697                .map(|s| s.to_owned());
698            let exp = json
699                .get("exp")
700                .and_then(|v| v.as_u64())
701                .map(|e| e * 1000)
702                .unwrap_or(0);
703            Some((origin, exp))
704        }
705        pub fn init(self: Arc<Self>) {
706            let this = self.clone();
707            tokio::spawn(async move {
708                let mut backoff = Duration::from_secs(1);
709                loop {
710                    if this.refresh_token().await.is_some() {
711                        break;
712                    }
713                    warn!(
714                        "Failed to initialize Apple Music token, retrying in {:?}...",
715                        backoff
716                    );
717                    tokio::time::sleep(backoff).await;
718                    backoff = (backoff * 2).min(Duration::from_secs(60));
719                }
720            });
721        }
722    }
723}
724use crate::{
725    protocol::tracks::LoadResult,
726    sources::{SourcePlugin, playable_track::BoxedTrack},
727};
728use async_trait::async_trait;
729use regex::Regex;
730use std::sync::Arc;
731use token::AppleMusicTokenTracker;
732const API_BASE: &str = "https://api.music.apple.com/v1";
733pub struct AppleMusicSource {
734    client: Arc<reqwest::Client>,
735    token_tracker: Arc<AppleMusicTokenTracker>,
736    country_code: String,
737    playlist_load_limit: usize,
738    album_load_limit: usize,
739    playlist_page_load_concurrency: usize,
740    album_page_load_concurrency: usize,
741    url_regex: Regex,
742}
743impl AppleMusicSource {
744    pub fn new(
745        config: Option<crate::config::AppleMusicConfig>,
746        client: Arc<reqwest::Client>,
747    ) -> Result<Self, String> {
748        let (country, p_limit, a_limit, p_conc, a_conc) = if let Some(c) = config {
749            (
750                c.country_code,
751                c.playlist_load_limit,
752                c.album_load_limit,
753                c.playlist_page_load_concurrency,
754                c.album_page_load_concurrency,
755            )
756        } else {
757            ("us".to_owned(), 0, 0, 5, 5)
758        };
759        let token_tracker = Arc::new(AppleMusicTokenTracker::new(client.clone()));
760        token_tracker.clone().init();
761        Ok(Self {
762            token_tracker,
763            client,
764            country_code: country,
765            playlist_load_limit: p_limit,
766            album_load_limit: a_limit,
767            playlist_page_load_concurrency: p_conc,
768            album_page_load_concurrency: a_conc,
769            url_regex: Regex::new(r"https?://(?:www\.)?music\.apple\.com/(?:[a-zA-Z]{2}/)?(album|playlist|artist|song)/[^/]+/([a-zA-Z0-9\-.]+)(?:\?i=(\d+))?").unwrap(),
770        })
771    }
772}
773#[async_trait]
774impl SourcePlugin for AppleMusicSource {
775    fn name(&self) -> &str {
776        "applemusic"
777    }
778    fn can_handle(&self, identifier: &str) -> bool {
779        self.search_prefixes()
780            .iter()
781            .any(|p| identifier.starts_with(p))
782            || self.url_regex.is_match(identifier)
783    }
784    fn search_prefixes(&self) -> Vec<&str> {
785        vec!["amsearch:"]
786    }
787    fn is_mirror(&self) -> bool {
788        true
789    }
790    async fn load(
791        &self,
792        identifier: &str,
793        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
794    ) -> LoadResult {
795        if let Some(prefix) = self
796            .search_prefixes()
797            .into_iter()
798            .find(|p| identifier.starts_with(p))
799        {
800            let query = &identifier[prefix.len()..];
801            return self.search(query).await;
802        }
803        if let Some(caps) = self.url_regex.captures(identifier) {
804            let type_str = caps.get(1).map(|m| m.as_str()).unwrap_or("");
805            let id = caps.get(2).map(|m| m.as_str()).unwrap_or("");
806            let song_id = caps.get(3).map(|m| m.as_str());
807            if type_str == "album"
808                && let Some(s_id) = song_id
809            {
810                return self.resolve_track(s_id).await;
811            }
812            match type_str {
813                "song" => return self.resolve_track(id).await,
814                "album" => return self.resolve_album(id).await,
815                "playlist" => return self.resolve_playlist(id).await,
816                "artist" => return self.resolve_artist(id).await,
817                _ => return LoadResult::Empty {},
818            }
819        }
820        LoadResult::Empty {}
821    }
822    async fn load_search(
823        &self,
824        query: &str,
825        types: &[String],
826        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
827    ) -> Option<crate::protocol::tracks::SearchResult> {
828        let q = if let Some(prefix) = self
829            .search_prefixes()
830            .into_iter()
831            .find(|p| query.starts_with(p))
832        {
833            &query[prefix.len()..]
834        } else {
835            query
836        };
837        self.get_search_suggestions(q, types).await
838    }
839    async fn get_track(
840        &self,
841        _identifier: &str,
842        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
843    ) -> Option<BoxedTrack> {
844        None
845    }
846}