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}