1pub mod api {
2 use super::validators::{is_invalid_album, is_invalid_playlist};
3 use serde_json::{Value, json};
4 use std::sync::Arc;
5 use tokio::sync::RwLock;
6 use tracing::{debug, warn};
7 const CONFIG_URL: &str = "https://music.amazon.com/config.json";
8 const API_BASE: &str = "https://eu.mesk.skill.music.a2z.com/api";
9 pub const EP_TRACK_INFO: &str = "cosmicTrack/displayCatalogTrack";
10 pub const EP_ALBUM_INFO: &str = "showCatalogAlbum";
11 pub const EP_ARTIST_INFO: &str = "explore/v1/showCatalogArtist";
12 pub const EP_PLAYLIST_INFO: &str = "showCatalogPlaylist";
13 pub const EP_COMMUNITY_PLAYLIST_INFO: &str = "showLibraryPlaylist";
14 pub const EP_TRACKS_SEARCH: &str = "searchCatalogTracks";
15 const USER_AGENT: &str = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36";
16 pub struct AmazonMusicClient {
17 http: Arc<reqwest::Client>,
18 cached_config: RwLock<Option<Value>>,
19 }
20 impl AmazonMusicClient {
21 pub fn new(http: Arc<reqwest::Client>) -> Self {
22 Self {
23 http,
24 cached_config: RwLock::new(None),
25 }
26 }
27 pub async fn site_config(&self) -> Option<Value> {
28 {
29 let guard = self.cached_config.read().await;
30 if guard.is_some() {
31 return guard.clone();
32 }
33 }
34 let resp = match self
35 .http
36 .get(CONFIG_URL)
37 .header("accept", "*/*")
38 .header("accept-language", "en-US,en;q=0.9")
39 .header("origin", "https://music.amazon.com")
40 .header("referer", "https://music.amazon.com/")
41 .header("user-agent", USER_AGENT)
42 .send()
43 .await
44 {
45 Ok(r) => r,
46 Err(e) => {
47 warn!("Amazon Music: config fetch failed: {e}");
48 return None;
49 }
50 };
51 if !resp.status().is_success() {
52 warn!("Amazon Music: config fetch HTTP {}", resp.status());
53 return None;
54 }
55 let config: Value = match resp.json().await {
56 Ok(v) => v,
57 Err(e) => {
58 debug!("Amazon Music: config JSON parse error: {e}");
59 return None;
60 }
61 };
62 *self.cached_config.write().await = Some(config.clone());
63 Some(config)
64 }
65 pub fn build_amzn_headers(config: &Value, page_url: &str) -> Value {
66 let access_token = config["accessToken"].as_str().unwrap_or("");
67 let device_id = config["deviceId"].as_str().unwrap_or("");
68 let session_id = config["sessionId"].as_str().unwrap_or("");
69 let version = config["version"].as_str().unwrap_or("");
70 let csrf_token = config["csrf"]["token"].as_str().unwrap_or("");
71 let csrf_ts = config["csrf"]["ts"]
72 .as_str()
73 .map(|s| s.to_string())
74 .or_else(|| config["csrf"]["ts"].as_u64().map(|n| n.to_string()))
75 .unwrap_or_default();
76 let csrf_rnd = config["csrf"]["rnd"]
77 .as_str()
78 .map(|s| s.to_string())
79 .or_else(|| config["csrf"]["rnd"].as_u64().map(|n| n.to_string()))
80 .unwrap_or_default();
81 let ts = std::time::SystemTime::now()
82 .duration_since(std::time::UNIX_EPOCH)
83 .map(|d| d.as_millis().to_string())
84 .unwrap_or_default();
85 let req_id = gen_request_id();
86 json!({
87 "x-amzn-authentication": serde_json::to_string(&json!({
88 "interface": "ClientAuthenticationInterface.v1_0.ClientTokenElement",
89 "accessToken": access_token
90 })).unwrap_or_default(),
91 "x-amzn-device-model": "WEBPLAYER",
92 "x-amzn-device-width": "1920",
93 "x-amzn-device-family": "WebPlayer",
94 "x-amzn-device-id": device_id,
95 "x-amzn-user-agent": USER_AGENT,
96 "x-amzn-session-id": session_id,
97 "x-amzn-device-height": "1080",
98 "x-amzn-request-id": req_id,
99 "x-amzn-device-language": "en_US",
100 "x-amzn-currency-of-preference": "USD",
101 "x-amzn-os-version": "1.0",
102 "x-amzn-application-version": version,
103 "x-amzn-device-time-zone": "Asia/Calcutta",
104 "x-amzn-timestamp": ts,
105 "x-amzn-csrf": serde_json::to_string(&json!({
106 "interface": "CSRFInterface.v1_0.CSRFHeaderElement",
107 "token": csrf_token,
108 "timestamp": csrf_ts,
109 "rndNonce": csrf_rnd
110 })).unwrap_or_default(),
111 "x-amzn-music-domain": "music.amazon.com",
112 "x-amzn-referer": "music.amazon.com",
113 "x-amzn-affiliate-tags": "",
114 "x-amzn-ref-marker": "",
115 "x-amzn-page-url": page_url,
116 "x-amzn-weblab-id-overrides": "",
117 "x-amzn-video-player-token": "",
118 "x-amzn-feature-flags": "hd-supported,uhd-supported",
119 "x-amzn-has-profile-id": "",
120 "x-amzn-age-band": ""
121 })
122 }
123 pub async fn post_endpoint(
124 &self,
125 path: &str,
126 mut body: Value,
127 page_url: &str,
128 ) -> Option<Value> {
129 let config = self.site_config().await?;
130 let amzn_headers = Self::build_amzn_headers(&config, page_url);
131 body["headers"] =
132 Value::String(serde_json::to_string(&amzn_headers).unwrap_or_default());
133 let url = format!("{API_BASE}/{path}");
134 let resp = match self
135 .http
136 .post(&url)
137 .header("authority", "eu.mesk.skill.music.a2z.com")
138 .header("accept", "*/*")
139 .header("accept-language", "en-US,en;q=0.9")
140 .header("content-type", "text/plain;charset=UTF-8")
141 .header("origin", "https://music.amazon.com")
142 .header("referer", "https://music.amazon.com/")
143 .header(
144 "sec-ch-ua",
145 "\"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
146 )
147 .header("sec-ch-ua-mobile", "?1")
148 .header("sec-ch-ua-platform", "\"Android\"")
149 .header("sec-fetch-dest", "empty")
150 .header("sec-fetch-mode", "cors")
151 .header("sec-fetch-site", "cross-site")
152 .header("user-agent", USER_AGENT)
153 .body(serde_json::to_string(&body).unwrap_or_default())
154 .send()
155 .await
156 {
157 Ok(r) => r,
158 Err(e) => {
159 debug!("Amazon Music: POST {path} failed: {e}");
160 return None;
161 }
162 };
163 if !resp.status().is_success() {
164 warn!("Amazon Music: POST {path} HTTP {}", resp.status());
165 *self.cached_config.write().await = None;
166 return None;
167 }
168 match resp.json::<Value>().await {
169 Ok(v) => Some(v),
170 Err(e) => {
171 debug!("Amazon Music: POST {path} JSON parse error: {e}");
172 None
173 }
174 }
175 }
176 fn entity_body(id: &str) -> Value {
177 json!({
178 "id": id,
179 "userHash": serde_json::to_string(&json!({"level": "LIBRARY_MEMBER"})).unwrap_or_default()
180 })
181 }
182 pub async fn fetch_track(&self, id: &str) -> Option<Value> {
183 let page = format!(
184 "https://music.amazon.com/tracks/{}",
185 urlencoding::encode(id)
186 );
187 self.post_endpoint(EP_TRACK_INFO, Self::entity_body(id), &page)
188 .await
189 }
190 pub async fn fetch_album(&self, id: &str) -> Option<Value> {
191 let page = format!(
192 "https://music.amazon.com/albums/{}",
193 urlencoding::encode(id)
194 );
195 self.post_endpoint(EP_ALBUM_INFO, Self::entity_body(id), &page)
196 .await
197 }
198 pub async fn fetch_artist(&self, id: &str) -> Option<Value> {
199 let page = format!(
200 "https://music.amazon.com/artists/{}",
201 urlencoding::encode(id)
202 );
203 self.post_endpoint(EP_ARTIST_INFO, Self::entity_body(id), &page)
204 .await
205 }
206 pub async fn fetch_playlist(&self, id: &str) -> Option<Value> {
207 let page = format!(
208 "https://music.amazon.com/playlists/{}",
209 urlencoding::encode(id)
210 );
211 self.post_endpoint(EP_PLAYLIST_INFO, Self::entity_body(id), &page)
212 .await
213 }
214 pub async fn fetch_community_playlist(&self, id: &str) -> Option<Value> {
215 let page = format!(
216 "https://music.amazon.com/user-playlists/{}",
217 urlencoding::encode(id)
218 );
219 self.post_endpoint(EP_COMMUNITY_PLAYLIST_INFO, Self::entity_body(id), &page)
220 .await
221 }
222 pub async fn search_tracks(&self, query: &str) -> Option<Value> {
223 let page = format!(
224 "https://music.amazon.com/search/{}/songs",
225 urlencoding::encode(query)
226 );
227 let body = json!({
228 "keyword": query,
229 "userHash": serde_json::to_string(&json!({"level": "LIBRARY_MEMBER"})).unwrap_or_default()
230 });
231 self.post_endpoint(EP_TRACKS_SEARCH, body, &page).await
232 }
233 pub async fn fetch_album_multi_region(
234 &self,
235 id: &str,
236 domain_hint: Option<&str>,
237 ) -> Option<Value> {
238 let config = self.site_config().await?;
239 super::region::fetch_multi_region(
240 &self.http,
241 id,
242 EP_ALBUM_INFO,
243 "albums",
244 "Album",
245 domain_hint,
246 is_invalid_album,
247 &config,
248 )
249 .await
250 }
251 pub async fn fetch_playlist_multi_region(
252 &self,
253 id: &str,
254 domain_hint: Option<&str>,
255 ) -> Option<Value> {
256 let config = self.site_config().await?;
257 super::region::fetch_multi_region(
258 &self.http,
259 id,
260 EP_PLAYLIST_INFO,
261 "playlists",
262 "Playlist",
263 domain_hint,
264 is_invalid_playlist,
265 &config,
266 )
267 .await
268 }
269 }
270 pub fn gen_request_id() -> String {
271 use std::time::{SystemTime, UNIX_EPOCH};
272 let seed = SystemTime::now()
273 .duration_since(UNIX_EPOCH)
274 .map(|d| d.subsec_nanos())
275 .unwrap_or(42);
276 let mut x = seed ^ (seed << 13);
277 x ^= x >> 17;
278 x ^= x << 5;
279 (0..13)
280 .map(|i| {
281 x ^= x.wrapping_add(i).wrapping_mul(1664525);
282 b"0123456789abcdefghijklmnopqrstuvwxyz"[(x as usize) % 36] as char
283 })
284 .collect()
285 }
286 pub fn duration_str_to_ms(s: &str) -> u64 {
287 let s = s.trim();
288 if s.is_empty() {
289 return 0;
290 }
291 let parts: Vec<u64> = s.split(':').filter_map(|p| p.trim().parse().ok()).collect();
292 let secs = match parts.as_slice() {
293 [h, m, s] => h * 3600 + m * 60 + s,
294 [first, second] => {
295 if *first >= 60 {
296 let h = first / 60;
297 let m = first % 60;
298 h * 3600 + m * 60 + second
299 } else {
300 first * 60 + second
301 }
302 }
303 [n] => *n,
304 _ => 0,
305 };
306 secs * 1000
307 }
308 pub fn clean_image_url(url: &str) -> String {
309 if url.is_empty() {
310 return url.to_owned();
311 }
312 if url.contains("_CLa%7C") {
313 return url
314 .replace("._AA", ".")
315 .replace("_US354", "_US1000")
316 .replace("CLa%7C354,354", "CLa%7C1000,1000")
317 .replace("0,0,354,354", "0,0,1000,1000")
318 .replace("0,0,177,177", "0,0,500,500")
319 .replace("177,0,177,177", "500,0,500,500")
320 .replace("0,177,177,177", "0,500,500,500")
321 .replace("177,177,177,177", "500,500,500,500");
322 }
323 if let Some(i_pos) = url.find("/I/") {
324 let after = &url[i_pos + 3..];
325 if let Some(dot_pos) = after.rfind('.') {
326 let ext = &after[dot_pos..];
327 let id_end = after.find(&['.', '_', '?'][..]).unwrap_or(after.len());
328 let id_part = &after[..id_end];
329 return format!("{}/I/{}{}", &url[..i_pos], id_part, ext);
330 }
331 }
332 url.to_owned()
333 }
334 pub fn clean_song_title(title: &str) -> String {
335 if let Some(stripped) = title.trim().strip_prefix(|c: char| c.is_ascii_digit()) {
336 let rest = stripped.trim_start_matches(|c: char| c.is_ascii_digit());
337 if let Some(clean) = rest.strip_prefix(". ") {
338 return clean.to_owned();
339 }
340 }
341 title.to_owned()
342 }
343 pub fn normalize_artist(raw: &str) -> String {
344 let parts: Vec<&str> = raw
345 .split(['&', ','])
346 .map(|p| p.trim())
347 .filter(|p| !p.is_empty())
348 .take(3)
349 .collect();
350 if parts.is_empty() {
351 return raw.trim().to_owned();
352 }
353 parts.join(", ")
354 }
355}
356pub mod crypt {
357 use aes::Aes128;
358 use ctr::Ctr128BE;
359 use tracing::{debug, warn};
360 type Aes128Ctr = Ctr128BE<Aes128>;
361 pub struct CencDecryptor {
362 key: [u8; 16],
363 }
364 impl CencDecryptor {
365 pub fn from_hex(hex_key: &str) -> Result<Self, String> {
366 let trimmed = hex_key.trim();
367 if trimmed.len() != 32 {
368 return Err(format!(
369 "decryption key must be 32 hex chars, got {}",
370 trimmed.len()
371 ));
372 }
373 let mut key = [0u8; 16];
374 hex::decode_to_slice(trimmed, &mut key).map_err(|e| format!("invalid hex key: {e}"))?;
375 Ok(Self { key })
376 }
377 pub fn decrypt_buffer(&self, buf: &mut [u8]) -> Result<(), String> {
378 patch_moov_headers(buf)?;
379 if let Ok(fragments) = locate_fragments(buf) {
380 for frag in fragments {
381 let _ = self.decrypt_fragment(buf, &frag);
382 }
383 }
384 Ok(())
385 }
386 fn decrypt_fragment(&self, buf: &mut [u8], frag: &FragmentInfo) -> Result<(), String> {
387 let mdat_payload_start = frag.mdat_offset + 8;
388 let mdat_payload_end = frag.mdat_offset + frag.mdat_size;
389 if mdat_payload_end > buf.len() {
390 return Err("mdat extends past buffer".into());
391 }
392 if frag.sample_ivs.is_empty() {
393 debug!("fragment has no sample IVs, skipping decryption");
394 return Ok(());
395 }
396 let mut cursor = mdat_payload_start;
397 for (idx, sample) in frag.samples.iter().enumerate() {
398 let iv_bytes = frag
399 .sample_ivs
400 .get(idx)
401 .ok_or_else(|| format!("missing IV for sample {idx}"))?;
402 let sample_end = cursor + sample.size;
403 if sample_end > mdat_payload_end || sample_end > buf.len() {
404 warn!("sample {} exceeds mdat boundary, stopping", idx);
405 break;
406 }
407 let mut full_iv = [0u8; 16];
408 let copy_len = iv_bytes.len().min(16);
409 full_iv[..copy_len].copy_from_slice(&iv_bytes[..copy_len]);
410 if !sample.subsamples.is_empty() {
411 let mut pos = cursor;
412 for sub in &sample.subsamples {
413 pos += sub.clear as usize;
414 let enc_len = sub.encrypted as usize;
415 if pos + enc_len > sample_end {
416 break;
417 }
418 self.ctr_decrypt(&full_iv, &mut buf[pos..pos + enc_len]);
419 pos += enc_len;
420 }
421 } else {
422 self.ctr_decrypt(&full_iv, &mut buf[cursor..sample_end]);
423 }
424 cursor = sample_end;
425 }
426 Ok(())
427 }
428 fn ctr_decrypt(&self, iv: &[u8; 16], data: &mut [u8]) {
429 self.ctr_decrypt_with_offset(iv, data, 0);
430 }
431 fn ctr_decrypt_with_offset(&self, iv: &[u8; 16], data: &mut [u8], offset: usize) {
432 use ctr::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
433 let mut cipher = Aes128Ctr::new(&self.key.into(), iv.into());
434 if offset > 0 {
435 cipher.seek(offset as u64);
436 }
437 cipher.apply_keystream(data);
438 }
439 pub fn ctr_decrypt_external(&self, iv_bytes: &[u8], data: &mut [u8], offset: usize) {
440 let mut full_iv = [0u8; 16];
441 let copy_len = iv_bytes.len().min(16);
442 full_iv[..copy_len].copy_from_slice(&iv_bytes[..copy_len]);
443 self.ctr_decrypt_with_offset(&full_iv, data, offset);
444 }
445 }
446 pub fn parse_moof_external(moof: &[u8]) -> (Vec<SampleEntry>, u8) {
447 parse_moof(moof)
448 }
449 pub fn extract_sample_ivs_external(
450 moof: &[u8],
451 iv_size_hint: u8,
452 count: usize,
453 ) -> Vec<Vec<u8>> {
454 extract_sample_ivs(moof, iv_size_hint, count)
455 }
456 struct FragmentInfo {
457 mdat_offset: usize,
458 mdat_size: usize,
459 samples: Vec<SampleEntry>,
460 sample_ivs: Vec<Vec<u8>>,
461 }
462 #[derive(Debug, Clone)]
463 pub struct SampleEntry {
464 pub size: usize,
465 pub subsamples: Vec<SubsampleEntry>,
466 }
467 #[derive(Debug, Clone)]
468 pub struct SubsampleEntry {
469 pub clear: u32,
470 pub encrypted: u32,
471 }
472 fn locate_fragments(buf: &[u8]) -> Result<Vec<FragmentInfo>, String> {
473 let mut frags = Vec::new();
474 let mut pos = 0;
475 while pos + 8 <= buf.len() {
476 let box_size =
477 u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
478 let box_type = &buf[pos + 4..pos + 8];
479 if box_size < 8 || pos + box_size > buf.len() {
480 break;
481 }
482 if box_type == b"moof" {
483 let moof_end = pos + box_size;
484 let mdat_offset = moof_end;
485 if mdat_offset + 8 > buf.len() {
486 pos += box_size;
487 continue;
488 }
489 let mdat_size_raw = u32::from_be_bytes([
490 buf[mdat_offset],
491 buf[mdat_offset + 1],
492 buf[mdat_offset + 2],
493 buf[mdat_offset + 3],
494 ]) as usize;
495 let mdat_type = &buf[mdat_offset + 4..mdat_offset + 8];
496 if mdat_type != b"mdat" || mdat_size_raw < 8 {
497 pos += box_size;
498 continue;
499 }
500 let (samples, per_sample_iv_size) = parse_moof(&buf[pos..moof_end]);
501 let ivs =
502 extract_sample_ivs(&buf[pos..moof_end], per_sample_iv_size, samples.len());
503 frags.push(FragmentInfo {
504 mdat_offset,
505 mdat_size: mdat_size_raw,
506 samples,
507 sample_ivs: ivs,
508 });
509 pos = mdat_offset + mdat_size_raw;
510 continue;
511 }
512 pos += box_size;
513 }
514 Ok(frags)
515 }
516 fn parse_moof(moof: &[u8]) -> (Vec<SampleEntry>, u8) {
517 let mut samples = Vec::new();
518 let mut default_sample_size: u32 = 0;
519 let mut per_sample_iv_size: u8 = 0;
520 let mut pos = 8;
521 while pos + 8 <= moof.len() {
522 let sz = u32::from_be_bytes([moof[pos], moof[pos + 1], moof[pos + 2], moof[pos + 3]])
523 as usize;
524 let typ = &moof[pos + 4..pos + 8];
525 if sz < 8 || pos + sz > moof.len() {
526 break;
527 }
528 if typ == b"traf" {
529 let (s, dsz, iv_sz) = parse_traf(&moof[pos..pos + sz]);
530 if !s.is_empty() {
531 samples = s;
532 }
533 if dsz > 0 {
534 default_sample_size = dsz;
535 }
536 if iv_sz > 0 {
537 per_sample_iv_size = iv_sz;
538 }
539 }
540 pos += sz;
541 }
542 if default_sample_size > 0 {
543 for s in &mut samples {
544 if s.size == 0 {
545 s.size = default_sample_size as usize;
546 }
547 }
548 }
549 (samples, per_sample_iv_size)
550 }
551 fn parse_traf(traf: &[u8]) -> (Vec<SampleEntry>, u32, u8) {
552 let mut samples = Vec::new();
553 let mut default_sample_size: u32 = 0;
554 let mut per_sample_iv_size: u8 = 0;
555 let mut pos = 8;
556 while pos + 8 <= traf.len() {
557 let sz = u32::from_be_bytes([traf[pos], traf[pos + 1], traf[pos + 2], traf[pos + 3]])
558 as usize;
559 let typ = &traf[pos + 4..pos + 8];
560 if sz < 8 || pos + sz > traf.len() {
561 break;
562 }
563 match typ {
564 b"tfhd" => {
565 default_sample_size = parse_tfhd_default_size(&traf[pos..pos + sz]);
566 }
567 b"trun" => {
568 samples = parse_trun(&traf[pos..pos + sz]);
569 }
570 b"senc" => {
571 let (ivs, subsubs) = parse_senc(&traf[pos..pos + sz], per_sample_iv_size);
572 for (i, (iv_data, subs)) in ivs.iter().zip(subsubs.iter()).enumerate() {
573 if i < samples.len() && !subs.is_empty() {
574 samples[i].subsamples = subs.clone();
575 }
576 let _ = iv_data;
577 }
578 }
579 b"sbgp" | b"sgpd" | b"saiz" | b"saio" => {}
580 _ => {}
581 }
582 pos += sz;
583 }
584 if per_sample_iv_size == 0 {
585 per_sample_iv_size = detect_iv_size_from_senc(traf, samples.len());
586 }
587 (samples, default_sample_size, per_sample_iv_size)
588 }
589 fn parse_tfhd_default_size(tfhd: &[u8]) -> u32 {
590 if tfhd.len() < 12 {
591 return 0;
592 }
593 let flags = u32::from_be_bytes([0, tfhd[9], tfhd[10], tfhd[11]]);
594 let mut off = 12;
595 off += 4;
596 if flags & 0x01 != 0 {
597 off += 8;
598 }
599 if flags & 0x02 != 0 {
600 off += 4;
601 }
602 if flags & 0x08 != 0 {
603 off += 4;
604 }
605 if flags & 0x10 != 0 && off + 4 <= tfhd.len() {
606 return u32::from_be_bytes([tfhd[off], tfhd[off + 1], tfhd[off + 2], tfhd[off + 3]]);
607 }
608 0
609 }
610 fn parse_trun(trun: &[u8]) -> Vec<SampleEntry> {
611 if trun.len() < 12 {
612 return Vec::new();
613 }
614 let flags = u32::from_be_bytes([0, trun[9], trun[10], trun[11]]);
615 let sample_count = u32::from_be_bytes([trun[12], trun[13], trun[14], trun[15]]) as usize;
616 let mut off = 16;
617 if flags & 0x01 != 0 {
618 off += 4;
619 }
620 if flags & 0x04 != 0 {
621 off += 4;
622 }
623 let has_duration = flags & 0x100 != 0;
624 let has_size = flags & 0x200 != 0;
625 let has_flags = flags & 0x400 != 0;
626 let has_cts = flags & 0x800 != 0;
627 let mut samples = Vec::with_capacity(sample_count);
628 for _ in 0..sample_count {
629 if has_duration {
630 off += 4;
631 }
632 let size = if has_size && off + 4 <= trun.len() {
633 let s =
634 u32::from_be_bytes([trun[off], trun[off + 1], trun[off + 2], trun[off + 3]]);
635 off += 4;
636 s as usize
637 } else {
638 if has_size {
639 off += 4;
640 }
641 0
642 };
643 if has_flags {
644 off += 4;
645 }
646 if has_cts {
647 off += 4;
648 }
649 samples.push(SampleEntry {
650 size,
651 subsamples: Vec::new(),
652 });
653 }
654 samples
655 }
656 fn parse_senc(senc: &[u8], iv_size_hint: u8) -> (Vec<Vec<u8>>, Vec<Vec<SubsampleEntry>>) {
657 if senc.len() < 12 {
658 return (Vec::new(), Vec::new());
659 }
660 let flags = u32::from_be_bytes([0, senc[9], senc[10], senc[11]]);
661 let sample_count = u32::from_be_bytes([senc[12], senc[13], senc[14], senc[15]]) as usize;
662 let has_subsamples = flags & 0x02 != 0;
663 let iv_size = if iv_size_hint > 0 {
664 iv_size_hint as usize
665 } else {
666 8
667 };
668 let mut off = 16;
669 let mut ivs = Vec::with_capacity(sample_count);
670 let mut all_subs = Vec::with_capacity(sample_count);
671 for _ in 0..sample_count {
672 if off + iv_size > senc.len() {
673 break;
674 }
675 ivs.push(senc[off..off + iv_size].to_vec());
676 off += iv_size;
677 let mut subs = Vec::new();
678 if has_subsamples {
679 if off + 2 > senc.len() {
680 break;
681 }
682 let sub_count = u16::from_be_bytes([senc[off], senc[off + 1]]) as usize;
683 off += 2;
684 for _ in 0..sub_count {
685 if off + 6 > senc.len() {
686 break;
687 }
688 let clear = u16::from_be_bytes([senc[off], senc[off + 1]]) as u32;
689 let encrypted = u32::from_be_bytes([
690 senc[off + 2],
691 senc[off + 3],
692 senc[off + 4],
693 senc[off + 5],
694 ]);
695 off += 6;
696 subs.push(SubsampleEntry { clear, encrypted });
697 }
698 }
699 all_subs.push(subs);
700 }
701 (ivs, all_subs)
702 }
703 fn detect_iv_size_from_senc(traf: &[u8], sample_count: usize) -> u8 {
704 if sample_count == 0 {
705 return 8;
706 }
707 let mut pos = 8;
708 while pos + 8 <= traf.len() {
709 let sz = u32::from_be_bytes([traf[pos], traf[pos + 1], traf[pos + 2], traf[pos + 3]])
710 as usize;
711 let typ = &traf[pos + 4..pos + 8];
712 if sz < 8 || pos + sz > traf.len() {
713 break;
714 }
715 if typ == b"senc" && sz >= 16 {
716 let flags = u32::from_be_bytes([0, traf[pos + 9], traf[pos + 10], traf[pos + 11]]);
717 let payload_after_header = sz - 16;
718 let has_sub = flags & 0x02 != 0;
719 if !has_sub && sample_count > 0 {
720 let candidate = payload_after_header / sample_count;
721 if candidate == 8 || candidate == 16 {
722 return candidate as u8;
723 }
724 }
725 }
726 pos += sz;
727 }
728 8
729 }
730 fn extract_sample_ivs(moof: &[u8], iv_size_hint: u8, sample_count: usize) -> Vec<Vec<u8>> {
731 let mut pos = 8;
732 while pos + 8 <= moof.len() {
733 let sz = u32::from_be_bytes([moof[pos], moof[pos + 1], moof[pos + 2], moof[pos + 3]])
734 as usize;
735 let typ = &moof[pos + 4..pos + 8];
736 if sz < 8 || pos + sz > moof.len() {
737 break;
738 }
739 if typ == b"traf" {
740 return extract_senc_ivs(&moof[pos..pos + sz], iv_size_hint, sample_count);
741 }
742 pos += sz;
743 }
744 Vec::new()
745 }
746 fn extract_senc_ivs(traf: &[u8], iv_size_hint: u8, _sample_count: usize) -> Vec<Vec<u8>> {
747 let mut pos = 8;
748 while pos + 8 <= traf.len() {
749 let sz = u32::from_be_bytes([traf[pos], traf[pos + 1], traf[pos + 2], traf[pos + 3]])
750 as usize;
751 let typ = &traf[pos + 4..pos + 8];
752 if sz < 8 || pos + sz > traf.len() {
753 break;
754 }
755 if typ == b"senc" {
756 let (ivs, _) = parse_senc(&traf[pos..pos + sz], iv_size_hint);
757 return ivs;
758 }
759 pos += sz;
760 }
761 Vec::new()
762 }
763 pub fn patch_moov_headers(buf: &mut [u8]) -> Result<(), String> {
764 let mut pos = 0;
765 while pos + 8 <= buf.len() {
766 let box_size =
767 u32::from_be_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
768 let box_type = &buf[pos + 4..pos + 8];
769 if box_size < 8 || pos + box_size > buf.len() {
770 break;
771 }
772 if box_type == b"moov" {
773 patch_moov_box(&mut buf[pos..pos + box_size]);
774 return Ok(());
775 }
776 pos += box_size;
777 }
778 Ok(())
779 }
780 fn patch_moov_box(moov: &mut [u8]) {
781 let mut pos = 8;
782 while pos + 8 <= moov.len() {
783 let sz = u32::from_be_bytes([moov[pos], moov[pos + 1], moov[pos + 2], moov[pos + 3]])
784 as usize;
785 if sz < 8 || pos + sz > moov.len() {
786 break;
787 }
788 let typ = [moov[pos + 4], moov[pos + 5], moov[pos + 6], moov[pos + 7]];
789 if &typ == b"trak" {
790 patch_trak(&mut moov[pos..pos + sz]);
791 }
792 pos += sz;
793 }
794 }
795 fn patch_trak(trak: &mut [u8]) {
796 let mut pos = 8;
797 while pos + 8 <= trak.len() {
798 let sz = u32::from_be_bytes([trak[pos], trak[pos + 1], trak[pos + 2], trak[pos + 3]])
799 as usize;
800 if sz < 8 || pos + sz > trak.len() {
801 break;
802 }
803 let typ = [trak[pos + 4], trak[pos + 5], trak[pos + 6], trak[pos + 7]];
804 if &typ == b"mdia" {
805 patch_mdia(&mut trak[pos..pos + sz]);
806 }
807 pos += sz;
808 }
809 }
810 fn patch_mdia(mdia: &mut [u8]) {
811 let mut pos = 8;
812 while pos + 8 <= mdia.len() {
813 let sz = u32::from_be_bytes([mdia[pos], mdia[pos + 1], mdia[pos + 2], mdia[pos + 3]])
814 as usize;
815 if sz < 8 || pos + sz > mdia.len() {
816 break;
817 }
818 let typ = [mdia[pos + 4], mdia[pos + 5], mdia[pos + 6], mdia[pos + 7]];
819 if &typ == b"minf" {
820 patch_minf(&mut mdia[pos..pos + sz]);
821 }
822 pos += sz;
823 }
824 }
825 fn patch_minf(minf: &mut [u8]) {
826 let mut pos = 8;
827 while pos + 8 <= minf.len() {
828 let sz = u32::from_be_bytes([minf[pos], minf[pos + 1], minf[pos + 2], minf[pos + 3]])
829 as usize;
830 if sz < 8 || pos + sz > minf.len() {
831 break;
832 }
833 let typ = [minf[pos + 4], minf[pos + 5], minf[pos + 6], minf[pos + 7]];
834 if &typ == b"stbl" {
835 patch_stbl(&mut minf[pos..pos + sz]);
836 }
837 pos += sz;
838 }
839 }
840 fn patch_stbl(stbl: &mut [u8]) {
841 let mut pos = 8;
842 while pos + 8 <= stbl.len() {
843 let sz = u32::from_be_bytes([stbl[pos], stbl[pos + 1], stbl[pos + 2], stbl[pos + 3]])
844 as usize;
845 if sz < 8 || pos + sz > stbl.len() {
846 break;
847 }
848 let typ = [stbl[pos + 4], stbl[pos + 5], stbl[pos + 6], stbl[pos + 7]];
849 if &typ == b"stsd" {
850 patch_stsd(&mut stbl[pos..pos + sz]);
851 }
852 pos += sz;
853 }
854 }
855 fn patch_stsd(stsd: &mut [u8]) {
856 if stsd.len() < 16 {
857 return;
858 }
859 stsd[12] = 0;
860 stsd[13] = 0;
861 stsd[14] = 0;
862 stsd[15] = 1;
863 let mut entry_pos = 16;
864 while entry_pos + 8 <= stsd.len() {
865 let entry_sz = u32::from_be_bytes([
866 stsd[entry_pos],
867 stsd[entry_pos + 1],
868 stsd[entry_pos + 2],
869 stsd[entry_pos + 3],
870 ]) as usize;
871 if entry_sz < 8 || entry_pos + entry_sz > stsd.len() {
872 break;
873 }
874 let codec_tag = [
875 stsd[entry_pos + 4],
876 stsd[entry_pos + 5],
877 stsd[entry_pos + 6],
878 stsd[entry_pos + 7],
879 ];
880 if &codec_tag == b"enca" {
881 let original = find_original_codec(&stsd[entry_pos..entry_pos + entry_sz]);
882 let replacement = original.unwrap_or(*b"mp4a");
883 debug!(
884 "patching stsd entry: enca -> {}",
885 std::str::from_utf8(&replacement).unwrap_or("????")
886 );
887 stsd[entry_pos + 4] = replacement[0];
888 stsd[entry_pos + 5] = replacement[1];
889 stsd[entry_pos + 6] = replacement[2];
890 stsd[entry_pos + 7] = replacement[3];
891 neutralize_sinf(&mut stsd[entry_pos..entry_pos + entry_sz]);
892 }
893 entry_pos += entry_sz;
894 }
895 }
896 fn find_original_codec(entry: &[u8]) -> Option<[u8; 4]> {
897 let mut pos = 36;
898 while pos + 8 <= entry.len() {
899 let sz =
900 u32::from_be_bytes([entry[pos], entry[pos + 1], entry[pos + 2], entry[pos + 3]])
901 as usize;
902 let typ = &entry[pos + 4..pos + 8];
903 if sz < 8 || pos + sz > entry.len() {
904 break;
905 }
906 if typ == b"sinf" {
907 return find_frma(&entry[pos..pos + sz]);
908 }
909 pos += sz;
910 }
911 None
912 }
913 fn find_frma(sinf: &[u8]) -> Option<[u8; 4]> {
914 let mut pos = 8;
915 while pos + 8 <= sinf.len() {
916 let sz = u32::from_be_bytes([sinf[pos], sinf[pos + 1], sinf[pos + 2], sinf[pos + 3]])
917 as usize;
918 let typ = &sinf[pos + 4..sinf.len().min(pos + 8)];
919 if sz < 8 || pos + sz > sinf.len() {
920 break;
921 }
922 if typ == b"frma" && sz >= 12 {
923 return Some([sinf[pos + 8], sinf[pos + 9], sinf[pos + 10], sinf[pos + 11]]);
924 }
925 pos += sz;
926 }
927 None
928 }
929 fn neutralize_sinf(entry: &mut [u8]) {
930 let mut pos = 36;
931 while pos + 8 <= entry.len() {
932 let sz =
933 u32::from_be_bytes([entry[pos], entry[pos + 1], entry[pos + 2], entry[pos + 3]])
934 as usize;
935 if sz < 8 || pos + sz > entry.len() {
936 break;
937 }
938 if &entry[pos + 4..pos + 8] == b"sinf" {
939 entry[pos + 4] = b'f';
940 entry[pos + 5] = b'r';
941 entry[pos + 6] = b'e';
942 entry[pos + 7] = b'e';
943 debug!("neutralized sinf box at offset {pos}");
944 }
945 pos += sz;
946 }
947 }
948 pub fn extract_flac_stream_header(moov: &[u8]) -> Option<Vec<u8>> {
949 let dfla = find_dfla_in_moov(moov)?;
950 if dfla.len() < 12 {
951 return None;
952 }
953 let metadata_blocks = &dfla[12..];
954 if metadata_blocks.is_empty() {
955 return None;
956 }
957 if metadata_blocks[0] & 0x7F != 0 {
958 warn!(
959 "Amazon FLAC: unexpected first metadata block type {}",
960 metadata_blocks[0] & 0x7F
961 );
962 return None;
963 }
964 let mut out = Vec::with_capacity(4 + metadata_blocks.len());
965 out.extend_from_slice(b"fLaC");
966 out.extend_from_slice(metadata_blocks);
967 ensure_last_metadata_block_flag(&mut out[4..]);
968 Some(out)
969 }
970 fn ensure_last_metadata_block_flag(metadata_blocks: &mut [u8]) {
971 let mut block_starts: Vec<usize> = Vec::new();
972 let mut pos = 0;
973 while pos + 4 <= metadata_blocks.len() {
974 let length = u32::from_be_bytes([
975 0,
976 metadata_blocks[pos + 1],
977 metadata_blocks[pos + 2],
978 metadata_blocks[pos + 3],
979 ]) as usize;
980 if pos + 4 + length > metadata_blocks.len() {
981 break;
982 }
983 block_starts.push(pos);
984 pos += 4 + length;
985 }
986 if block_starts.is_empty() {
987 return;
988 }
989 for &start in &block_starts {
990 metadata_blocks[start] &= 0x7F;
991 }
992 let last = *block_starts.last().unwrap();
993 metadata_blocks[last] |= 0x80;
994 }
995 fn find_dfla_in_moov(moov: &[u8]) -> Option<&[u8]> {
996 let trak = find_child(moov, b"trak")?;
997 let mdia = find_child(trak, b"mdia")?;
998 let minf = find_child(mdia, b"minf")?;
999 let stbl = find_child(minf, b"stbl")?;
1000 let stsd = find_child(stbl, b"stsd")?;
1001 if stsd.len() < 16 {
1002 return None;
1003 }
1004 let audio_entry = {
1005 let entries = &stsd[16..];
1006 let mut pos = 0;
1007 loop {
1008 if pos + 8 > entries.len() {
1009 return None;
1010 }
1011 let sz = u32::from_be_bytes([
1012 entries[pos],
1013 entries[pos + 1],
1014 entries[pos + 2],
1015 entries[pos + 3],
1016 ]) as usize;
1017 if sz < 8 || pos + sz > entries.len() {
1018 return None;
1019 }
1020 let tag = &entries[pos + 4..pos + 8];
1021 if tag == b"enca" || tag == b"fLaC" {
1022 break &entries[pos..pos + sz];
1023 }
1024 pos += sz;
1025 }
1026 };
1027 if audio_entry.len() < 36 {
1028 return None;
1029 }
1030 find_box_in(&audio_entry[36..], b"dfLa")
1031 }
1032 fn find_child<'a>(parent: &'a [u8], target: &[u8; 4]) -> Option<&'a [u8]> {
1033 if parent.len() < 8 {
1034 return None;
1035 }
1036 find_box_in(&parent[8..], target)
1037 }
1038 fn find_box_in<'a>(data: &'a [u8], target: &[u8; 4]) -> Option<&'a [u8]> {
1039 let mut pos = 0;
1040 while pos + 8 <= data.len() {
1041 let sz = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
1042 as usize;
1043 if sz < 8 || pos + sz > data.len() {
1044 break;
1045 }
1046 if &data[pos + 4..pos + 8] == target {
1047 return Some(&data[pos..pos + sz]);
1048 }
1049 pos += sz;
1050 }
1051 None
1052 }
1053}
1054pub mod direct {
1055 use super::streaming_reader::AmazonStreamingReader;
1056 use crate::{
1057 audio::source::create_client,
1058 config::HttpProxyConfig,
1059 sources::playable_track::{PlayableTrack, ResolvedTrack},
1060 };
1061 use async_trait::async_trait;
1062 use std::net::IpAddr;
1063 use tracing::debug;
1064 pub const UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
1065 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
1066 pub struct AmazonMusicTrack {
1067 pub track_id: String,
1068 pub stream_url: String,
1069 pub decryption_key: String,
1070 pub local_addr: Option<IpAddr>,
1071 pub proxy: Option<HttpProxyConfig>,
1072 }
1073 #[async_trait]
1074 impl PlayableTrack for AmazonMusicTrack {
1075 fn supports_seek(&self) -> bool {
1076 true
1077 }
1078 async fn resolve(&self) -> Result<ResolvedTrack, String> {
1079 debug!(
1080 "Amazon Music: opening streaming reader for {}",
1081 self.track_id
1082 );
1083 let stream_client =
1084 create_client(UA.to_owned(), self.local_addr, self.proxy.clone(), None)
1085 .map_err(|e| format!("failed to create client: {e}"))?;
1086 let content_len = probe_content_length(&stream_client, &self.stream_url)
1087 .await
1088 .map_err(|e| format!("failed to probe stream length: {e}"))?;
1089 let streaming_reader = AmazonStreamingReader::new(
1090 stream_client,
1091 &self.stream_url,
1092 &self.decryption_key,
1093 content_len,
1094 )
1095 .map_err(|e| format!("failed to initialize streaming reader: {e}"))?;
1096 let reader = Box::new(streaming_reader) as Box<dyn symphonia::core::io::MediaSource>;
1097 Ok(ResolvedTrack::new(reader, None))
1098 }
1099 }
1100 async fn probe_content_length(client: &reqwest::Client, url: &str) -> Result<u64, String> {
1101 let head = client
1102 .head(url)
1103 .header("User-Agent", UA)
1104 .send()
1105 .await
1106 .map_err(|e| format!("HEAD request failed: {e}"))?;
1107 if let Some(len) = head.content_length() {
1108 return Ok(len);
1109 }
1110 let range_resp = client
1111 .get(url)
1112 .header("User-Agent", UA)
1113 .header("Range", "bytes=0-0")
1114 .send()
1115 .await
1116 .map_err(|e| format!("range probe failed: {e}"))?;
1117 range_resp
1118 .headers()
1119 .get(reqwest::header::CONTENT_RANGE)
1120 .and_then(|v| v.to_str().ok())
1121 .and_then(|s| s.split('/').next_back())
1122 .and_then(|s| s.parse::<u64>().ok())
1123 .ok_or_else(|| "could not determine stream length".to_string())
1124 }
1125}
1126pub mod manager {
1127 use super::{
1128 api::AmazonMusicClient,
1129 direct::AmazonMusicTrack,
1130 parsers::{
1131 parse_album_tracks, parse_artist_top_songs, parse_community_playlist_tracks,
1132 parse_playlist_tracks, parse_search_tracks, parse_track,
1133 },
1134 validators::{
1135 is_invalid_album, is_invalid_artist, is_invalid_community_playlist,
1136 is_invalid_playlist, is_invalid_track,
1137 },
1138 };
1139 use crate::{
1140 config::AmazonMusicConfig,
1141 protocol::tracks::{LoadError, LoadResult, PlaylistData, PlaylistInfo, Track},
1142 sources::{SourcePlugin, playable_track::BoxedTrack},
1143 };
1144 use async_trait::async_trait;
1145 use regex::Regex;
1146 use serde_json::json;
1147 use std::{net::IpAddr, sync::Arc};
1148 use tracing::debug;
1149 #[derive(serde::Deserialize)]
1150 struct StreamApiResponse {
1151 #[serde(rename = "streamUrl")]
1152 stream_url: String,
1153 #[serde(rename = "decryptionKey")]
1154 decryption_key: String,
1155 }
1156 const TRACK_RE: &str =
1157 r"(?i)^https?://(?:www\.)?music\.amazon\.[a-z.]+/tracks/([A-Z0-9]{10,20})";
1158 const ALBUM_RE: &str =
1159 r"(?i)^https?://(?:www\.)?music\.amazon\.[a-z.]+/albums/([A-Z0-9]{10,20})";
1160 const ARTIST_RE: &str =
1161 r"(?i)^https?://(?:www\.)?music\.amazon\.[a-z.]+/artists/([A-Z0-9]{10,20})";
1162 const PLAYLIST_RE: &str =
1163 r"(?i)^https?://(?:www\.)?music\.amazon\.[a-z.]+/playlists/([A-Z0-9]{10,20})";
1164 const USER_PLAYLIST_RE: &str =
1165 r"(?i)^https?://(?:www\.)?music\.amazon\.[a-z.]+/user-playlists/([a-zA-Z0-9]+)";
1166 const DOMAIN_RE: &str = r"(?i)^https?://(?:www\.)?music\.amazon\.";
1167 pub struct AmazonMusicSource {
1168 client: Arc<AmazonMusicClient>,
1169 http: Arc<reqwest::Client>,
1170 search_limit: usize,
1171 proxy: Option<crate::config::HttpProxyConfig>,
1172 api_url: Option<String>,
1173 local_addr: Option<IpAddr>,
1174 track_re: Regex,
1175 album_re: Regex,
1176 artist_re: Regex,
1177 playlist_re: Regex,
1178 user_playlist_re: Regex,
1179 domain_re: Regex,
1180 }
1181 impl AmazonMusicSource {
1182 pub fn new(config: AmazonMusicConfig, http: Arc<reqwest::Client>) -> Result<Self, String> {
1183 Ok(Self {
1184 client: Arc::new(AmazonMusicClient::new(Arc::clone(&http))),
1185 http,
1186 search_limit: config.search_limit.min(5),
1187 proxy: config.proxy,
1188 api_url: config.api_url,
1189 local_addr: None,
1190 track_re: Regex::new(TRACK_RE).map_err(|e| e.to_string())?,
1191 album_re: Regex::new(ALBUM_RE).map_err(|e| e.to_string())?,
1192 artist_re: Regex::new(ARTIST_RE).map_err(|e| e.to_string())?,
1193 playlist_re: Regex::new(PLAYLIST_RE).map_err(|e| e.to_string())?,
1194 user_playlist_re: Regex::new(USER_PLAYLIST_RE).map_err(|e| e.to_string())?,
1195 domain_re: Regex::new(DOMAIN_RE).map_err(|e| e.to_string())?,
1196 })
1197 }
1198 fn capture_id(&self, re: &Regex, url: &str) -> Option<String> {
1199 re.captures(url)
1200 .and_then(|c| c.get(1))
1201 .map(|m| m.as_str().to_string())
1202 }
1203 async fn load_track(&self, url: &str) -> LoadResult {
1204 let id = match self.capture_id(&self.track_re, url) {
1205 Some(id) => id,
1206 None => return LoadResult::Empty {},
1207 };
1208 let resp = match self.client.fetch_track(&id).await {
1209 Some(v) => v,
1210 None => {
1211 return LoadResult::Error(LoadError {
1212 message: Some(format!("Amazon Music: failed to fetch track '{id}'")),
1213 severity: crate::common::Severity::Suspicious,
1214 cause: "API request failed".to_string(),
1215 cause_stack_trace: None,
1216 });
1217 }
1218 };
1219 if is_invalid_track(&resp) {
1220 debug!("Amazon Music: track '{id}' not found or no longer available");
1221 return LoadResult::Empty {};
1222 }
1223 match parse_track(&resp, &id) {
1224 Some(info) => LoadResult::Track(Track::new(info)),
1225 None => {
1226 debug!("Amazon Music: failed to parse track '{id}'");
1227 LoadResult::Empty {}
1228 }
1229 }
1230 }
1231 async fn load_album(&self, url: &str) -> LoadResult {
1232 let album_id = match self.capture_id(&self.album_re, url) {
1233 Some(id) => id,
1234 None => return LoadResult::Empty {},
1235 };
1236 let domain_hint = super::region::extract_domain(url);
1237 let resp = match self
1238 .client
1239 .fetch_album_multi_region(&album_id, domain_hint.as_deref())
1240 .await
1241 {
1242 Some(v) => v,
1243 None => return LoadResult::Empty {},
1244 };
1245 if is_invalid_album(&resp) {
1246 debug!("Amazon Music: album '{album_id}' not found");
1247 return LoadResult::Empty {};
1248 }
1249 let (album_name, artist_name, track_infos) = match parse_album_tracks(&resp, &album_id)
1250 {
1251 Some(r) => r,
1252 None => return LoadResult::Empty {},
1253 };
1254 let artwork = resp["methods"][0]["template"]["headerImage"]
1255 .as_str()
1256 .filter(|s| !s.is_empty())
1257 .map(super::api::clean_image_url);
1258 let tracks: Vec<Track> = track_infos.into_iter().map(Track::new).collect();
1259 if tracks.is_empty() {
1260 return LoadResult::Empty {};
1261 }
1262 LoadResult::Playlist(PlaylistData {
1263 info: PlaylistInfo {
1264 name: album_name.clone(),
1265 selected_track: -1,
1266 },
1267 plugin_info: json!({
1268 "type": "album",
1269 "url": format!("https://music.amazon.com/albums/{album_id}"),
1270 "artworkUrl": artwork,
1271 "author": artist_name,
1272 "totalTracks": tracks.len()
1273 }),
1274 tracks,
1275 })
1276 }
1277 async fn load_artist(&self, url: &str) -> LoadResult {
1278 let artist_id = match self.capture_id(&self.artist_re, url) {
1279 Some(id) => id,
1280 None => return LoadResult::Empty {},
1281 };
1282 let resp = match self.client.fetch_artist(&artist_id).await {
1283 Some(v) => v,
1284 None => return LoadResult::Empty {},
1285 };
1286 if is_invalid_artist(&resp) {
1287 debug!("Amazon Music: artist '{artist_id}' not found");
1288 return LoadResult::Empty {};
1289 }
1290 let unique_album_ids: Vec<String> = {
1291 let widgets = match resp["methods"][0]["template"]["widgets"].as_array() {
1292 Some(w) => w,
1293 None => return LoadResult::Empty {},
1294 };
1295 let top_songs = match widgets.iter().find(|w| {
1296 w["header"]
1297 .as_str()
1298 .map(|h| h.to_lowercase().contains("top songs"))
1299 .unwrap_or(false)
1300 }) {
1301 Some(w) => w,
1302 None => return LoadResult::Empty {},
1303 };
1304 let mut seen = std::collections::HashSet::new();
1305 top_songs["items"]
1306 .as_array()
1307 .map(|items| {
1308 items
1309 .iter()
1310 .filter_map(|item| {
1311 let key = item["iconButton"]["observer"]["storageKey"].as_str()?;
1312 let album_id = key.split(':').next()?.to_string();
1313 if !album_id.is_empty() && seen.insert(album_id.clone()) {
1314 Some(album_id)
1315 } else {
1316 None
1317 }
1318 })
1319 .collect()
1320 })
1321 .unwrap_or_default()
1322 };
1323 let fetch_futures: Vec<_> = unique_album_ids
1324 .iter()
1325 .map(|album_id| self.client.fetch_album(album_id))
1326 .collect();
1327 let album_responses = futures::future::join_all(fetch_futures).await;
1328 let mut duration_map: std::collections::HashMap<String, u64> =
1329 std::collections::HashMap::new();
1330 for (album_id, album_resp) in unique_album_ids.iter().zip(album_responses) {
1331 let album_resp = match album_resp {
1332 Some(v) => v,
1333 None => continue,
1334 };
1335 let album_items =
1336 match album_resp["methods"][0]["template"]["widgets"][0]["items"].as_array() {
1337 Some(i) => i.clone(),
1338 None => continue,
1339 };
1340 for track in &album_items {
1341 let deeplink = match track["primaryTextLink"]["deeplink"].as_str() {
1342 Some(dl) => dl,
1343 None => continue,
1344 };
1345 let track_id = match deeplink.split("/tracks/").nth(1) {
1346 Some(id) => id.split('/').next().unwrap_or("").to_string(),
1347 None => continue,
1348 };
1349 if track_id.is_empty() {
1350 continue;
1351 }
1352 let duration_ms = super::api::duration_str_to_ms(
1353 track["secondaryText3"].as_str().unwrap_or(""),
1354 );
1355 duration_map.insert(format!("{album_id}:{track_id}"), duration_ms);
1356 }
1357 }
1358 let result = match parse_artist_top_songs(&resp, &artist_id, &duration_map) {
1359 Some(r) => r,
1360 None => return LoadResult::Empty {},
1361 };
1362 let tracks: Vec<Track> = result.tracks.into_iter().map(Track::new).collect();
1363 if tracks.is_empty() {
1364 return LoadResult::Empty {};
1365 }
1366 LoadResult::Playlist(PlaylistData {
1367 info: PlaylistInfo {
1368 name: format!("{}'s Top Songs", result.name),
1369 selected_track: -1,
1370 },
1371 plugin_info: json!({
1372 "type": "artist",
1373 "url": format!("https://music.amazon.com/artists/{artist_id}"),
1374 "artworkUrl": result.artwork_url,
1375 "author": result.name,
1376 "totalTracks": tracks.len()
1377 }),
1378 tracks,
1379 })
1380 }
1381 async fn load_playlist(&self, url: &str) -> LoadResult {
1382 let playlist_id = match self.capture_id(&self.playlist_re, url) {
1383 Some(id) => id,
1384 None => return LoadResult::Empty {},
1385 };
1386 let domain_hint = super::region::extract_domain(url);
1387 let resp = match self
1388 .client
1389 .fetch_playlist_multi_region(&playlist_id, domain_hint.as_deref())
1390 .await
1391 {
1392 Some(v) => v,
1393 None => return LoadResult::Empty {},
1394 };
1395 if is_invalid_playlist(&resp) {
1396 debug!("Amazon Music: playlist '{playlist_id}' not found/unavailable");
1397 return LoadResult::Empty {};
1398 }
1399 let result = match parse_playlist_tracks(&resp) {
1400 Some(r) => r,
1401 None => return LoadResult::Empty {},
1402 };
1403 let tracks: Vec<Track> = result.tracks.into_iter().map(Track::new).collect();
1404 if tracks.is_empty() {
1405 return LoadResult::Empty {};
1406 }
1407 LoadResult::Playlist(PlaylistData {
1408 info: PlaylistInfo {
1409 name: result.name.clone(),
1410 selected_track: -1,
1411 },
1412 plugin_info: json!({
1413 "type": "playlist",
1414 "url": format!("https://music.amazon.com/playlists/{playlist_id}"),
1415 "artworkUrl": result.artwork_url,
1416 "author": "Amazon Music",
1417 "totalTracks": tracks.len()
1418 }),
1419 tracks,
1420 })
1421 }
1422 async fn load_community_playlist(&self, url: &str) -> LoadResult {
1423 let playlist_id = match self.capture_id(&self.user_playlist_re, url) {
1424 Some(id) => id,
1425 None => return LoadResult::Empty {},
1426 };
1427 let resp = match self.client.fetch_community_playlist(&playlist_id).await {
1428 Some(v) => v,
1429 None => return LoadResult::Empty {},
1430 };
1431 if is_invalid_community_playlist(&resp) {
1432 debug!("Amazon Music: community playlist '{playlist_id}' not found");
1433 return LoadResult::Empty {};
1434 }
1435 let result = match parse_community_playlist_tracks(&resp) {
1436 Some(r) => r,
1437 None => return LoadResult::Empty {},
1438 };
1439 let tracks: Vec<Track> = result.tracks.into_iter().map(Track::new).collect();
1440 if tracks.is_empty() {
1441 return LoadResult::Empty {};
1442 }
1443 LoadResult::Playlist(PlaylistData {
1444 info: PlaylistInfo {
1445 name: result.name.clone(),
1446 selected_track: -1,
1447 },
1448 plugin_info: json!({
1449 "type": "playlist",
1450 "url": format!("https://music.amazon.com/user-playlists/{playlist_id}"),
1451 "artworkUrl": result.artwork_url,
1452 "author": "Community User",
1453 "totalTracks": tracks.len()
1454 }),
1455 tracks,
1456 })
1457 }
1458 async fn load_search(&self, query: &str) -> LoadResult {
1459 let resp = match self.client.search_tracks(query).await {
1460 Some(v) => v,
1461 None => return LoadResult::Empty {},
1462 };
1463 let items: Vec<serde_json::Value> = match resp["methods"]
1464 .as_array()
1465 .and_then(|m| m.first())
1466 .and_then(|m| m["template"]["widgets"].as_array())
1467 .and_then(|w| w.first())
1468 .and_then(|w| w["items"].as_array())
1469 {
1470 Some(i) => i.iter().take(self.search_limit).cloned().collect(),
1471 None => return LoadResult::Empty {},
1472 };
1473 let mut unique_albums: std::collections::HashSet<String> =
1474 std::collections::HashSet::new();
1475 for item in &items {
1476 if let Some(key) = item["iconButton"]["observer"]["storageKey"].as_str()
1477 && let Some(album_id) = key.split(':').next()
1478 && !album_id.is_empty()
1479 {
1480 unique_albums.insert(album_id.to_string());
1481 }
1482 }
1483 let album_ids: Vec<String> = unique_albums.into_iter().collect();
1484 let mut duration_map: std::collections::HashMap<String, u64> =
1485 std::collections::HashMap::new();
1486 for batch in album_ids.chunks(5) {
1487 let futures: Vec<_> = batch
1488 .iter()
1489 .map(|album_id| self.client.fetch_album(album_id))
1490 .collect();
1491 let results = futures::future::join_all(futures).await;
1492 for (album_id, album_resp) in batch.iter().zip(results) {
1493 let album_resp = match album_resp {
1494 Some(v) => v,
1495 None => continue,
1496 };
1497 let album_items =
1498 match album_resp["methods"][0]["template"]["widgets"][0]["items"].as_array()
1499 {
1500 Some(i) => i.clone(),
1501 None => continue,
1502 };
1503 for track in &album_items {
1504 let deeplink = match track["primaryTextLink"]["deeplink"].as_str() {
1505 Some(dl) => dl,
1506 None => continue,
1507 };
1508 let track_id = match deeplink.split("/tracks/").nth(1) {
1509 Some(id) => id.split('/').next().unwrap_or("").to_string(),
1510 None => continue,
1511 };
1512 if track_id.is_empty() {
1513 continue;
1514 }
1515 let duration_ms = super::api::duration_str_to_ms(
1516 track["secondaryText3"].as_str().unwrap_or(""),
1517 );
1518 duration_map.insert(format!("{album_id}:{track_id}"), duration_ms);
1519 }
1520 }
1521 }
1522 let tracks: Vec<Track> = parse_search_tracks(&resp, self.search_limit, &duration_map)
1523 .into_iter()
1524 .map(Track::new)
1525 .collect();
1526 if tracks.is_empty() {
1527 LoadResult::Empty {}
1528 } else {
1529 LoadResult::Search(tracks)
1530 }
1531 }
1532 }
1533 #[async_trait]
1534 impl SourcePlugin for AmazonMusicSource {
1535 fn name(&self) -> &str {
1536 "amazonmusic"
1537 }
1538 fn can_handle(&self, identifier: &str) -> bool {
1539 self.search_prefixes()
1540 .iter()
1541 .any(|p| identifier.starts_with(p))
1542 || self.domain_re.is_match(identifier)
1543 }
1544 fn search_prefixes(&self) -> Vec<&str> {
1545 vec!["azmsearch:", "amznsearch:"]
1546 }
1547 async fn load(
1548 &self,
1549 identifier: &str,
1550 _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
1551 ) -> LoadResult {
1552 if let Some(prefix) = self
1553 .search_prefixes()
1554 .into_iter()
1555 .find(|p| identifier.starts_with(p))
1556 {
1557 return self.load_search(&identifier[prefix.len()..]).await;
1558 }
1559 if self.track_re.is_match(identifier) {
1560 return self.load_track(identifier).await;
1561 }
1562 if self.album_re.is_match(identifier) {
1563 return self.load_album(identifier).await;
1564 }
1565 if self.artist_re.is_match(identifier) {
1566 return self.load_artist(identifier).await;
1567 }
1568 if self.user_playlist_re.is_match(identifier) {
1569 return self.load_community_playlist(identifier).await;
1570 }
1571 if self.playlist_re.is_match(identifier) {
1572 return self.load_playlist(identifier).await;
1573 }
1574 LoadResult::Empty {}
1575 }
1576 async fn get_track(
1577 &self,
1578 identifier: &str,
1579 routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
1580 ) -> Option<BoxedTrack> {
1581 let api_base = match self.api_url.as_ref() {
1582 Some(url) => url,
1583 None => {
1584 tracing::debug!("AmazonMusic: api_url not set, falling back to mirror");
1585 return None;
1586 }
1587 };
1588 let track_id = self
1589 .capture_id(&self.track_re, identifier)
1590 .unwrap_or_else(|| identifier.to_string());
1591 let api_endpoint = format!("{}/api/track/{}", api_base.trim_end_matches('/'), track_id);
1592 let response = match self
1593 .http
1594 .get(&api_endpoint)
1595 .header("User-Agent", super::direct::UA)
1596 .send()
1597 .await
1598 {
1599 Ok(res) => {
1600 if !res.status().is_success() {
1601 tracing::warn!("AmazonMusic API returned error status: {}", res.status());
1602 return None;
1603 }
1604 match res.json::<StreamApiResponse>().await {
1605 Ok(data) => data,
1606 Err(e) => {
1607 tracing::warn!("AmazonMusic API failed to parse JSON: {}", e);
1608 return None;
1609 }
1610 }
1611 }
1612 Err(e) => {
1613 tracing::warn!("AmazonMusic API request failed: {}", e);
1614 return None;
1615 }
1616 };
1617 if response.stream_url.is_empty() {
1618 tracing::warn!("AmazonMusic API returned empty stream URL");
1619 return None;
1620 }
1621 let local_addr = routeplanner
1622 .as_ref()
1623 .and_then(|rp| rp.get_address())
1624 .or(self.local_addr);
1625 tracing::info!(
1626 "AmazonMusic: Direct playback configured successfully for {}",
1627 track_id
1628 );
1629 Some(Arc::new(AmazonMusicTrack {
1630 track_id,
1631 stream_url: response.stream_url,
1632 decryption_key: response.decryption_key,
1633 local_addr,
1634 proxy: self.proxy.clone(),
1635 }))
1636 }
1637 fn get_proxy_config(&self) -> Option<crate::config::HttpProxyConfig> {
1638 self.proxy.clone()
1639 }
1640 }
1641}
1642pub mod parsers {
1643 use super::api::{clean_image_url, clean_song_title, duration_str_to_ms, normalize_artist};
1644 use crate::protocol::tracks::TrackInfo;
1645 use serde_json::Value;
1646 pub fn parse_track(resp: &Value, track_id: &str) -> Option<TrackInfo> {
1647 let methods = resp["methods"].as_array()?;
1648 let template = &methods.first()?["template"];
1649 if template.is_null() {
1650 return None;
1651 }
1652 let widgets = template["widgets"].as_array()?;
1653 let tracklist = widgets.iter().find(|w| {
1654 w["header"]
1655 .as_str()
1656 .map(|h| h.to_lowercase().contains("album tracklist"))
1657 .unwrap_or(false)
1658 })?;
1659 let items = tracklist["items"].as_array()?;
1660 let track_item = items.iter().find(|item| {
1661 item["primaryTextLink"]["deeplink"]
1662 .as_str()
1663 .map(|dl| dl.contains(&format!("/tracks/{track_id}")))
1664 .unwrap_or(false)
1665 })?;
1666 let title = template["headerText"]["text"]
1667 .as_str()
1668 .unwrap_or("Unknown Title")
1669 .to_string();
1670 let artist = normalize_artist(
1671 template["headerPrimaryText"]
1672 .as_str()
1673 .unwrap_or("Unknown Artist"),
1674 );
1675 let duration_ms = duration_str_to_ms(track_item["secondaryText3"].as_str().unwrap_or(""));
1676 let artwork_url = template["headerImage"]
1677 .as_str()
1678 .filter(|s| !s.is_empty())
1679 .map(clean_image_url);
1680 let isrc = extract_isrc(template);
1681 Some(TrackInfo {
1682 identifier: track_id.to_string(),
1683 is_seekable: true,
1684 author: artist,
1685 length: duration_ms,
1686 is_stream: false,
1687 position: 0,
1688 title,
1689 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1690 artwork_url,
1691 isrc,
1692 source_name: "amazonmusic".to_string(),
1693 })
1694 }
1695 pub fn parse_album_tracks(
1696 resp: &Value,
1697 _album_id: &str,
1698 ) -> Option<(String, String, Vec<TrackInfo>)> {
1699 let template = &resp["methods"][0]["template"];
1700 let album_name = template["headerText"]["text"]
1701 .as_str()
1702 .unwrap_or("Unknown Album")
1703 .to_string();
1704 let artist_name = template["headerPrimaryText"]
1705 .as_str()
1706 .unwrap_or("Unknown Artist")
1707 .to_string();
1708 let artwork = template["headerImage"].as_str().unwrap_or("").to_string();
1709 let items = template["widgets"][0]["items"]
1710 .as_array()
1711 .cloned()
1712 .unwrap_or_default();
1713 let tracks = items
1714 .iter()
1715 .filter_map(|item| {
1716 let track_id = item["primaryTextLink"]["deeplink"]
1717 .as_str()
1718 .and_then(|dl| dl.split("/tracks/").nth(1))?
1719 .to_string();
1720 let title = item["primaryText"]
1721 .as_str()
1722 .unwrap_or("Unknown Title")
1723 .to_string();
1724 let item_artist = normalize_artist(
1725 item["secondaryText2"]
1726 .as_str()
1727 .filter(|s| !s.is_empty())
1728 .unwrap_or(&artist_name),
1729 );
1730 let duration_ms = duration_str_to_ms(item["secondaryText3"].as_str().unwrap_or(""));
1731 let art = if artwork.is_empty() {
1732 None
1733 } else {
1734 Some(clean_image_url(&artwork))
1735 };
1736 Some(TrackInfo {
1737 identifier: track_id.clone(),
1738 is_seekable: true,
1739 author: item_artist,
1740 length: duration_ms,
1741 is_stream: false,
1742 position: 0,
1743 title,
1744 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1745 artwork_url: art,
1746 isrc: None,
1747 source_name: "amazonmusic".to_string(),
1748 })
1749 })
1750 .collect();
1751 Some((album_name, artist_name, tracks))
1752 }
1753 pub struct ArtistResult {
1754 pub name: String,
1755 pub artwork_url: Option<String>,
1756 pub tracks: Vec<TrackInfo>,
1757 }
1758 pub fn parse_artist_top_songs(
1759 resp: &Value,
1760 artist_id: &str,
1761 duration_map: &std::collections::HashMap<String, u64>,
1762 ) -> Option<ArtistResult> {
1763 let template = &resp["methods"][0]["template"];
1764 let artist_name = template["headerText"]["text"]
1765 .as_str()
1766 .unwrap_or("Unknown Artist")
1767 .to_string();
1768 let artwork_url = template["backgroundImage"]
1769 .as_str()
1770 .filter(|s| !s.is_empty())
1771 .map(clean_image_url);
1772 let widgets = template["widgets"].as_array()?;
1773 let top_songs_widget = widgets.iter().find(|w| {
1774 w["header"]
1775 .as_str()
1776 .map(|h| h.to_lowercase().contains("top songs"))
1777 .unwrap_or(false)
1778 })?;
1779 let items = top_songs_widget["items"].as_array()?;
1780 let tracks = items
1781 .iter()
1782 .filter_map(|item| {
1783 let storage_key = item["iconButton"]["observer"]["storageKey"].as_str()?;
1784 let mut parts = storage_key.splitn(2, ':');
1785 let album_id = parts.next()?.to_string();
1786 let track_id = parts.next()?.to_string();
1787 if track_id.is_empty() {
1788 return None;
1789 }
1790 let title = clean_song_title(
1791 item["primaryText"]["text"]
1792 .as_str()
1793 .unwrap_or("Unknown Title"),
1794 );
1795 let artist =
1796 normalize_artist(item["secondaryText"].as_str().unwrap_or(&artist_name));
1797 let item_artwork = item["image"]
1798 .as_str()
1799 .filter(|s| !s.is_empty())
1800 .map(clean_image_url)
1801 .or_else(|| artwork_url.clone());
1802 let duration_ms = duration_map
1803 .get(&format!("{album_id}:{track_id}"))
1804 .copied()
1805 .unwrap_or(0);
1806 Some(TrackInfo {
1807 identifier: track_id.clone(),
1808 is_seekable: true,
1809 author: artist,
1810 length: duration_ms,
1811 is_stream: false,
1812 position: 0,
1813 title,
1814 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1815 artwork_url: item_artwork,
1816 isrc: None,
1817 source_name: "amazonmusic".to_string(),
1818 })
1819 })
1820 .collect();
1821 let _ = artist_id;
1822 Some(ArtistResult {
1823 name: artist_name,
1824 artwork_url,
1825 tracks,
1826 })
1827 }
1828 pub struct PlaylistResult {
1829 pub name: String,
1830 pub artwork_url: Option<String>,
1831 pub tracks: Vec<TrackInfo>,
1832 }
1833 pub fn parse_playlist_tracks(resp: &Value) -> Option<PlaylistResult> {
1834 let template = &resp["methods"][0]["template"];
1835 let name = template["headerText"]["text"]
1836 .as_str()
1837 .unwrap_or("Unknown Playlist")
1838 .to_string();
1839 let artwork_url = template["headerImage"]
1840 .as_str()
1841 .filter(|s| !s.is_empty())
1842 .map(clean_image_url);
1843 let items = template["widgets"][0]["items"]
1844 .as_array()
1845 .cloned()
1846 .unwrap_or_default();
1847 let tracks = items
1848 .iter()
1849 .filter_map(|item| {
1850 let storage_key = item["iconButton"]["observer"]["storageKey"].as_str()?;
1851 let mut parts = storage_key.splitn(2, ':');
1852 let _album_id = parts.next()?;
1853 let track_id = parts.next()?.to_string();
1854 if track_id.is_empty() {
1855 return None;
1856 }
1857 let title = item["primaryText"]
1858 .as_str()
1859 .unwrap_or("Unknown Title")
1860 .to_string();
1861 let artist =
1862 normalize_artist(item["secondaryText1"].as_str().unwrap_or("Unknown Artist"));
1863 let duration_ms = duration_str_to_ms(item["secondaryText3"].as_str().unwrap_or(""));
1864 let item_art = item["image"]
1865 .as_str()
1866 .filter(|s| !s.is_empty())
1867 .map(clean_image_url)
1868 .or_else(|| artwork_url.clone());
1869 Some(TrackInfo {
1870 identifier: track_id.clone(),
1871 is_seekable: true,
1872 author: artist,
1873 length: duration_ms,
1874 is_stream: false,
1875 position: 0,
1876 title,
1877 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1878 artwork_url: item_art,
1879 isrc: None,
1880 source_name: "amazonmusic".to_string(),
1881 })
1882 })
1883 .collect();
1884 Some(PlaylistResult {
1885 name,
1886 artwork_url,
1887 tracks,
1888 })
1889 }
1890 pub fn parse_community_playlist_tracks(resp: &Value) -> Option<PlaylistResult> {
1891 let template = &resp["methods"][0]["template"];
1892 let name = template["headerText"]["text"]
1893 .as_str()
1894 .unwrap_or("Unknown Playlist")
1895 .to_string();
1896 let artwork_url = template["headerImage"]
1897 .as_str()
1898 .filter(|s| !s.is_empty())
1899 .map(clean_image_url);
1900 let items = template["widgets"][0]["items"]
1901 .as_array()
1902 .cloned()
1903 .unwrap_or_default();
1904 let tracks = items
1905 .iter()
1906 .filter_map(|item| {
1907 let track_id = item["id"].as_str()?.to_string();
1908 if track_id.is_empty() {
1909 return None;
1910 }
1911 let title = item["primaryText"]
1912 .as_str()
1913 .unwrap_or("Unknown Title")
1914 .to_string();
1915 let artist =
1916 normalize_artist(item["secondaryText1"].as_str().unwrap_or("Unknown Artist"));
1917 let duration_ms = duration_str_to_ms(item["secondaryText3"].as_str().unwrap_or(""));
1918 let item_art = item["image"]
1919 .as_str()
1920 .filter(|s| !s.is_empty())
1921 .map(clean_image_url)
1922 .or_else(|| artwork_url.clone());
1923 Some(TrackInfo {
1924 identifier: track_id.clone(),
1925 is_seekable: true,
1926 author: artist,
1927 length: duration_ms,
1928 is_stream: false,
1929 position: 0,
1930 title,
1931 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1932 artwork_url: item_art,
1933 isrc: None,
1934 source_name: "amazonmusic".to_string(),
1935 })
1936 })
1937 .collect();
1938 Some(PlaylistResult {
1939 name,
1940 artwork_url,
1941 tracks,
1942 })
1943 }
1944 pub fn parse_search_tracks(
1945 resp: &Value,
1946 limit: usize,
1947 duration_map: &std::collections::HashMap<String, u64>,
1948 ) -> Vec<TrackInfo> {
1949 let items = match resp["methods"]
1950 .as_array()
1951 .and_then(|m| m.first())
1952 .and_then(|m| m["template"]["widgets"].as_array())
1953 .and_then(|w| w.first())
1954 .and_then(|w| w["items"].as_array())
1955 {
1956 Some(i) => i,
1957 None => return Vec::new(),
1958 };
1959 items
1960 .iter()
1961 .take(limit)
1962 .filter_map(|item| {
1963 let storage_key = item["iconButton"]["observer"]["storageKey"].as_str()?;
1964 let mut parts = storage_key.splitn(2, ':');
1965 let album_id = parts.next()?.to_string();
1966 let track_id = parts.next()?.to_string();
1967 if track_id.is_empty() {
1968 return None;
1969 }
1970 let title = item["primaryText"]["text"]
1971 .as_str()
1972 .unwrap_or("Unknown Title")
1973 .to_string();
1974 let artist =
1975 normalize_artist(item["secondaryText"].as_str().unwrap_or("Unknown Artist"));
1976 let artwork = item["image"]
1977 .as_str()
1978 .filter(|s| !s.is_empty())
1979 .map(clean_image_url);
1980 let duration_ms = duration_map
1981 .get(&format!("{album_id}:{track_id}"))
1982 .copied()
1983 .unwrap_or(0);
1984 Some(TrackInfo {
1985 identifier: track_id.clone(),
1986 is_seekable: true,
1987 author: artist,
1988 length: duration_ms,
1989 is_stream: false,
1990 position: 0,
1991 title,
1992 uri: Some(format!("https://music.amazon.com/tracks/{track_id}")),
1993 artwork_url: artwork,
1994 isrc: None,
1995 source_name: "amazonmusic".to_string(),
1996 })
1997 })
1998 .collect()
1999 }
2000 fn extract_isrc(template: &Value) -> Option<String> {
2001 let scripts = template["templateData"]["seoHead"]["script"].as_array()?;
2002 for script in scripts {
2003 let inner_html = script["innerHTML"].as_str()?;
2004 if let Ok(parsed) = serde_json::from_str::<Value>(inner_html)
2005 && let Some(isrc) = parsed["isrcCode"].as_str()
2006 {
2007 return Some(isrc.to_string());
2008 }
2009 }
2010 None
2011 }
2012}
2013pub mod reader {
2014 use crate::{
2015 audio::source::{HttpSource, create_client},
2016 common::types::AnyResult,
2017 };
2018 use std::io::{Read, Seek, SeekFrom};
2019 use symphonia::core::io::MediaSource;
2020 const UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
2021 pub struct AmazonRemoteReader {
2022 inner: HttpSource,
2023 }
2024 impl AmazonRemoteReader {
2025 pub async fn open(
2026 url: &str,
2027 local_addr: Option<std::net::IpAddr>,
2028 proxy: Option<crate::config::HttpProxyConfig>,
2029 ) -> AnyResult<Self> {
2030 let client = create_client(UA.to_owned(), local_addr, proxy, None)?;
2031 let inner = HttpSource::new(client, url).await?;
2032 Ok(Self { inner })
2033 }
2034 }
2035 impl Read for AmazonRemoteReader {
2036 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2037 self.inner.read(buf)
2038 }
2039 }
2040 impl Seek for AmazonRemoteReader {
2041 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2042 self.inner.seek(pos)
2043 }
2044 }
2045 impl MediaSource for AmazonRemoteReader {
2046 fn is_seekable(&self) -> bool {
2047 self.inner.is_seekable()
2048 }
2049 fn byte_len(&self) -> Option<u64> {
2050 self.inner.byte_len()
2051 }
2052 }
2053}
2054pub mod region {
2055 use super::api::gen_request_id;
2056 use futures::{StreamExt, stream::FuturesUnordered};
2057 use regex::Regex;
2058 use serde_json::{Value, json};
2059 use std::{sync::LazyLock, time::Duration};
2060 use tracing::{debug, warn};
2061 const USER_AGENT: &str = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 \
2062 (KHTML, like Gecko) Chrome/125.0.0.0 Mobile Safari/537.36";
2063 pub struct RegionConfig {
2064 pub skill_endpoint: &'static str,
2065 pub language: &'static str,
2066 pub currency: &'static str,
2067 pub device_family: &'static str,
2068 pub feature_flags: &'static str,
2069 }
2070 const EP_NA: &str = "https://na.web.skill.music.a2z.com";
2071 const EP_EU: &str = "https://eu.web.skill.music.a2z.com";
2072 const EP_FE: &str = "https://fe.web.skill.music.a2z.com";
2073 static REGION_CONFIGS: &[(&str, RegionConfig)] = &[
2074 (
2075 "music.amazon.com",
2076 RegionConfig {
2077 skill_endpoint: EP_NA,
2078 language: "en_US",
2079 currency: "USD",
2080 device_family: "WebPlayer",
2081 feature_flags: "",
2082 },
2083 ),
2084 (
2085 "music.amazon.com.mx",
2086 RegionConfig {
2087 skill_endpoint: EP_NA,
2088 language: "es_MX",
2089 currency: "MXN",
2090 device_family: "WebPlayer",
2091 feature_flags: "",
2092 },
2093 ),
2094 (
2095 "music.amazon.com.br",
2096 RegionConfig {
2097 skill_endpoint: EP_NA,
2098 language: "pt_BR",
2099 currency: "BRL",
2100 device_family: "WebPlayer",
2101 feature_flags: "",
2102 },
2103 ),
2104 (
2105 "music.amazon.ca",
2106 RegionConfig {
2107 skill_endpoint: EP_NA,
2108 language: "en_CA",
2109 currency: "CAD",
2110 device_family: "WebPlayer",
2111 feature_flags: "",
2112 },
2113 ),
2114 (
2115 "music.amazon.co.uk",
2116 RegionConfig {
2117 skill_endpoint: EP_EU,
2118 language: "en_GB",
2119 currency: "GBP",
2120 device_family: "WebPlayer",
2121 feature_flags: "hd-supported,uhd-supported",
2122 },
2123 ),
2124 (
2125 "music.amazon.de",
2126 RegionConfig {
2127 skill_endpoint: EP_EU,
2128 language: "de_DE",
2129 currency: "EUR",
2130 device_family: "WebPlayer",
2131 feature_flags: "hd-supported,uhd-supported",
2132 },
2133 ),
2134 (
2135 "music.amazon.fr",
2136 RegionConfig {
2137 skill_endpoint: EP_EU,
2138 language: "fr_FR",
2139 currency: "EUR",
2140 device_family: "WebPlayer",
2141 feature_flags: "hd-supported,uhd-supported",
2142 },
2143 ),
2144 (
2145 "music.amazon.it",
2146 RegionConfig {
2147 skill_endpoint: EP_EU,
2148 language: "it_IT",
2149 currency: "EUR",
2150 device_family: "WebPlayer",
2151 feature_flags: "hd-supported,uhd-supported",
2152 },
2153 ),
2154 (
2155 "music.amazon.es",
2156 RegionConfig {
2157 skill_endpoint: EP_EU,
2158 language: "es_ES",
2159 currency: "EUR",
2160 device_family: "WebPlayer",
2161 feature_flags: "hd-supported,uhd-supported",
2162 },
2163 ),
2164 (
2165 "music.amazon.in",
2166 RegionConfig {
2167 skill_endpoint: EP_EU,
2168 language: "en_IN",
2169 currency: "INR",
2170 device_family: "WebPlayer",
2171 feature_flags: "hd-supported,uhd-supported",
2172 },
2173 ),
2174 (
2175 "music.amazon.sa",
2176 RegionConfig {
2177 skill_endpoint: EP_EU,
2178 language: "ar_SA",
2179 currency: "SAR",
2180 device_family: "WebPlayer",
2181 feature_flags: "",
2182 },
2183 ),
2184 (
2185 "music.amazon.ae",
2186 RegionConfig {
2187 skill_endpoint: EP_EU,
2188 language: "ar_AE",
2189 currency: "AED",
2190 device_family: "WebPlayer",
2191 feature_flags: "",
2192 },
2193 ),
2194 (
2195 "music.amazon.co.jp",
2196 RegionConfig {
2197 skill_endpoint: EP_FE,
2198 language: "ja_JP",
2199 currency: "JPY",
2200 device_family: "WebPlayer",
2201 feature_flags: "hd-supported,uhd-supported",
2202 },
2203 ),
2204 (
2205 "music.amazon.com.au",
2206 RegionConfig {
2207 skill_endpoint: EP_FE,
2208 language: "en_AU",
2209 currency: "AUD",
2210 device_family: "WebPlayer",
2211 feature_flags: "hd-supported,uhd-supported",
2212 },
2213 ),
2214 ];
2215 static REGION_FALLBACKS: &[(&str, &str)] = &[
2216 ("NA", "music.amazon.com"),
2217 ("EU", "music.amazon.co.uk"),
2218 ("FE", "music.amazon.co.jp"),
2219 ];
2220 fn get_region_config(domain: &str) -> Option<&'static RegionConfig> {
2221 REGION_CONFIGS
2222 .iter()
2223 .find(|(d, _)| *d == domain)
2224 .map(|(_, cfg)| cfg)
2225 }
2226 static DOMAIN_RE: LazyLock<Regex> =
2227 LazyLock::new(|| Regex::new(r"(?i)^https?://(?:www\.)?(music\.amazon\.[a-z.]+)").unwrap());
2228 pub fn extract_domain(url: &str) -> Option<String> {
2229 let caps = DOMAIN_RE.captures(url)?;
2230 let domain = caps.get(1)?.as_str().to_lowercase();
2231 if get_region_config(&domain).is_some() {
2232 Some(domain)
2233 } else {
2234 None
2235 }
2236 }
2237 fn build_region_headers(
2238 config: &Value,
2239 region: &RegionConfig,
2240 domain: &str,
2241 page_url: &str,
2242 ) -> Value {
2243 let access_token = config["accessToken"].as_str().unwrap_or("");
2244 let device_id = config["deviceId"].as_str().unwrap_or("");
2245 let session_id = config["sessionId"].as_str().unwrap_or("");
2246 let version = config["version"].as_str().unwrap_or("");
2247 let csrf_token = config["csrf"]["token"].as_str().unwrap_or("");
2248 let csrf_ts = config["csrf"]["ts"]
2249 .as_str()
2250 .map(|s| s.to_string())
2251 .or_else(|| config["csrf"]["ts"].as_u64().map(|n| n.to_string()))
2252 .unwrap_or_default();
2253 let csrf_rnd = config["csrf"]["rnd"]
2254 .as_str()
2255 .map(|s| s.to_string())
2256 .or_else(|| config["csrf"]["rnd"].as_u64().map(|n| n.to_string()))
2257 .unwrap_or_default();
2258 let ts = std::time::SystemTime::now()
2259 .duration_since(std::time::UNIX_EPOCH)
2260 .map(|d| d.as_millis().to_string())
2261 .unwrap_or_default();
2262 json!({
2263 "x-amzn-authentication": serde_json::to_string(&json!({
2264 "interface": "ClientAuthenticationInterface.v1_0.ClientTokenElement",
2265 "accessToken": access_token
2266 })).unwrap_or_default(),
2267 "x-amzn-device-model": "WEBPLAYER",
2268 "x-amzn-device-width": "1920",
2269 "x-amzn-device-family": region.device_family,
2270 "x-amzn-device-id": device_id,
2271 "x-amzn-user-agent": USER_AGENT,
2272 "x-amzn-session-id": session_id,
2273 "x-amzn-device-height": "1080",
2274 "x-amzn-request-id": gen_request_id(),
2275 "x-amzn-device-language": region.language,
2276 "x-amzn-currency-of-preference": region.currency,
2277 "x-amzn-os-version": "1.0",
2278 "x-amzn-application-version": version,
2279 "x-amzn-device-time-zone": "Asia/Calcutta",
2280 "x-amzn-timestamp": ts,
2281 "x-amzn-csrf": serde_json::to_string(&json!({
2282 "interface": "CSRFInterface.v1_0.CSRFHeaderElement",
2283 "token": csrf_token,
2284 "timestamp": csrf_ts,
2285 "rndNonce": csrf_rnd
2286 })).unwrap_or_default(),
2287 "x-amzn-music-domain": domain,
2288 "x-amzn-referer": domain,
2289 "x-amzn-affiliate-tags": "",
2290 "x-amzn-ref-marker": "",
2291 "x-amzn-page-url": page_url,
2292 "x-amzn-weblab-id-overrides": "",
2293 "x-amzn-video-player-token": "",
2294 "x-amzn-feature-flags": region.feature_flags,
2295 "x-amzn-has-profile-id": "",
2296 "x-amzn-age-band": ""
2297 })
2298 }
2299 async fn fetch_from_endpoint(
2300 http: &reqwest::Client,
2301 id: &str,
2302 api_path: &str,
2303 url_path_segment: &str,
2304 region: &RegionConfig,
2305 domain: &str,
2306 base_config: &Value,
2307 ) -> Option<Value> {
2308 let page_url = format!(
2309 "https://{domain}/{url_path_segment}/{}",
2310 urlencoding::encode(id)
2311 );
2312 let inner_headers = build_region_headers(base_config, region, domain, &page_url);
2313 let body = json!({
2314 "id": id,
2315 "userHash": serde_json::to_string(&json!({"level": "LIBRARY_MEMBER"})).unwrap_or_default(),
2316 "headers": serde_json::to_string(&inner_headers).unwrap_or_default()
2317 });
2318 let endpoint_url = format!("{}/api/{api_path}", region.skill_endpoint);
2319 let authority = region
2320 .skill_endpoint
2321 .trim_start_matches("https://")
2322 .trim_start_matches("http://");
2323 let resp = http
2324 .post(&endpoint_url)
2325 .header("authority", authority)
2326 .header("accept", "*/*")
2327 .header("accept-language", "en-US,en;q=0.9")
2328 .header("content-type", "text/plain;charset=UTF-8")
2329 .header("origin", format!("https://{domain}"))
2330 .header("referer", format!("https://{domain}/"))
2331 .header(
2332 "sec-ch-ua",
2333 "\"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
2334 )
2335 .header("sec-ch-ua-mobile", "?1")
2336 .header("sec-ch-ua-platform", "\"Android\"")
2337 .header("sec-fetch-dest", "empty")
2338 .header("sec-fetch-mode", "cors")
2339 .header("sec-fetch-site", "cross-site")
2340 .header("user-agent", USER_AGENT)
2341 .timeout(Duration::from_secs(8))
2342 .body(serde_json::to_string(&body).unwrap_or_default())
2343 .send()
2344 .await
2345 .ok()?;
2346 if !resp.status().is_success() {
2347 return None;
2348 }
2349 resp.json::<Value>().await.ok()
2350 }
2351 #[allow(clippy::too_many_arguments)]
2352 pub async fn fetch_multi_region(
2353 http: &reqwest::Client,
2354 id: &str,
2355 api_path: &str,
2356 url_path_segment: &str,
2357 entity_name: &str,
2358 domain_hint: Option<&str>,
2359 is_error: fn(&Value) -> bool,
2360 base_config: &Value,
2361 ) -> Option<Value> {
2362 let mut tried_endpoint: Option<&str> = None;
2363 if let Some(hint) = domain_hint
2364 && let Some(region) = get_region_config(hint)
2365 {
2366 debug!("Amazon Music: trying {entity_name} on hinted domain '{hint}'");
2367 tried_endpoint = Some(region.skill_endpoint);
2368 if let Some(data) = fetch_from_endpoint(
2369 http,
2370 id,
2371 api_path,
2372 url_path_segment,
2373 region,
2374 hint,
2375 base_config,
2376 )
2377 .await
2378 && !is_error(&data)
2379 {
2380 debug!("Amazon Music: {entity_name} resolved via hinted domain '{hint}'");
2381 return Some(data);
2382 }
2383 debug!(
2384 "Amazon Music: hinted domain '{hint}' failed for {entity_name}, trying other regions"
2385 );
2386 }
2387 let mut futs = FuturesUnordered::new();
2388 for &(label, domain) in REGION_FALLBACKS {
2389 let region = match get_region_config(domain) {
2390 Some(r) => r,
2391 None => continue,
2392 };
2393 if tried_endpoint == Some(region.skill_endpoint) {
2394 continue;
2395 }
2396 let base_config = base_config.clone();
2397 futs.push(async move {
2398 let result = fetch_from_endpoint(
2399 http,
2400 id,
2401 api_path,
2402 url_path_segment,
2403 region,
2404 domain,
2405 &base_config,
2406 )
2407 .await;
2408 result.map(|data| (data, label))
2409 });
2410 }
2411 while let Some(result) = futs.next().await {
2412 if let Some((data, label)) = result
2413 && !is_error(&data)
2414 {
2415 debug!("Amazon Music: {entity_name} resolved via {label} region");
2416 return Some(data);
2417 }
2418 }
2419 warn!("Amazon Music: {entity_name} not found on any regional endpoint (NA, EU, FE)");
2420 None
2421 }
2422}
2423pub mod streaming_reader {
2424 use super::crypt::{CencDecryptor, extract_flac_stream_header};
2425 use parking_lot::{Condvar, Mutex};
2426 use std::{
2427 io::{Read, Seek, SeekFrom},
2428 sync::Arc,
2429 time::Duration,
2430 };
2431 use symphonia::core::io::MediaSource;
2432 use tracing::{debug, warn};
2433 struct BufferState {
2434 data: Vec<u8>,
2435 available: usize,
2436 finished: bool,
2437 error: Option<String>,
2438 }
2439 pub struct AmazonStreamingReader {
2440 cursor: u64,
2441 shared: Arc<(Mutex<BufferState>, Condvar)>,
2442 }
2443 impl AmazonStreamingReader {
2444 pub fn new(
2445 client: reqwest::Client,
2446 url: &str,
2447 decryption_key: &str,
2448 total_len: u64,
2449 ) -> Result<Self, String> {
2450 let decryptor = CencDecryptor::from_hex(decryption_key)?;
2451 let shared = Arc::new((
2452 Mutex::new(BufferState {
2453 data: Vec::new(),
2454 available: 0,
2455 finished: false,
2456 error: None,
2457 }),
2458 Condvar::new(),
2459 ));
2460 let shared_bg = Arc::clone(&shared);
2461 let url_owned = url.to_string();
2462 std::thread::Builder::new()
2463 .name("amz-flac-extract".into())
2464 .spawn(move || {
2465 tokio::runtime::Builder::new_current_thread()
2466 .enable_all()
2467 .build()
2468 .unwrap()
2469 .block_on(download_and_extract_flac(
2470 shared_bg, client, url_owned, decryptor, total_len,
2471 ));
2472 })
2473 .map_err(|e| format!("failed to spawn stream thread: {e}"))?;
2474 {
2475 let (lock, cvar) = &*shared;
2476 let mut state = lock.lock();
2477 while state.available == 0 && !state.finished && state.error.is_none() {
2478 cvar.wait_for(&mut state, Duration::from_millis(50));
2479 }
2480 if let Some(ref e) = state.error {
2481 return Err(e.clone());
2482 }
2483 if state.available == 0 {
2484 return Err("stream ended before FLAC header could be extracted".into());
2485 }
2486 }
2487 Ok(Self { cursor: 0, shared })
2488 }
2489 }
2490 impl Read for AmazonStreamingReader {
2491 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2492 let (lock, cvar) = &*self.shared;
2493 let mut state = lock.lock();
2494 loop {
2495 if let Some(ref err) = state.error {
2496 return Err(std::io::Error::other(err.clone()));
2497 }
2498 let pos = self.cursor as usize;
2499 if pos < state.available {
2500 let n = (state.available - pos).min(buf.len());
2501 buf[..n].copy_from_slice(&state.data[pos..pos + n]);
2502 self.cursor += n as u64;
2503 return Ok(n);
2504 }
2505 if state.finished {
2506 return Ok(0);
2507 }
2508 cvar.wait_for(&mut state, Duration::from_millis(50));
2509 }
2510 }
2511 }
2512 impl Seek for AmazonStreamingReader {
2513 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
2514 let target: u64 = match pos {
2515 SeekFrom::Start(p) => p,
2516 SeekFrom::Current(d) => self.cursor.saturating_add_signed(d),
2517 SeekFrom::End(d) => {
2518 let (lock, cvar) = &*self.shared;
2519 let mut state = lock.lock();
2520 while !state.finished && state.error.is_none() {
2521 cvar.wait_for(&mut state, Duration::from_millis(50));
2522 }
2523 if let Some(ref e) = state.error {
2524 return Err(std::io::Error::other(e.clone()));
2525 }
2526 (state.available as i64).saturating_add(d).max(0) as u64
2527 }
2528 };
2529 {
2530 let (lock, cvar) = &*self.shared;
2531 let mut state = lock.lock();
2532 while (state.available as u64) < target && !state.finished && state.error.is_none()
2533 {
2534 cvar.wait_for(&mut state, Duration::from_millis(50));
2535 }
2536 if let Some(ref e) = state.error {
2537 return Err(std::io::Error::other(e.clone()));
2538 }
2539 self.cursor = target.min(state.available as u64);
2540 }
2541 Ok(self.cursor)
2542 }
2543 }
2544 impl MediaSource for AmazonStreamingReader {
2545 fn is_seekable(&self) -> bool {
2546 true
2547 }
2548 fn byte_len(&self) -> Option<u64> {
2549 let (lock, _) = &*self.shared;
2550 let state = lock.lock();
2551 if state.finished {
2552 Some(state.available as u64)
2553 } else {
2554 None
2555 }
2556 }
2557 }
2558 impl Drop for AmazonStreamingReader {
2559 fn drop(&mut self) {
2560 let (lock, _) = &*self.shared;
2561 lock.lock().finished = true;
2562 }
2563 }
2564 const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
2565 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
2566 async fn download_and_extract_flac(
2567 shared: Arc<(Mutex<BufferState>, Condvar)>,
2568 client: reqwest::Client,
2569 url: String,
2570 decryptor: CencDecryptor,
2571 _total_len: u64,
2572 ) {
2573 let response = match client
2574 .get(&url)
2575 .header("User-Agent", USER_AGENT)
2576 .header("Accept", "*/*")
2577 .send()
2578 .await
2579 {
2580 Ok(r) if r.status().is_success() => r,
2581 Ok(r) => {
2582 signal_error(&shared, format!("HTTP {}", r.status()));
2583 return;
2584 }
2585 Err(e) => {
2586 signal_error(&shared, format!("request failed: {e}"));
2587 return;
2588 }
2589 };
2590 let mut staging: Vec<u8> = Vec::with_capacity(256 * 1024);
2591 let mut total_drained: usize = 0;
2592 let mut remainder: Vec<u8> = Vec::new();
2593 let mut stream = response;
2594 let mut header_written = false;
2595 loop {
2596 match stream.chunk().await {
2597 Ok(Some(chunk)) => {
2598 staging.extend_from_slice(&chunk);
2599 }
2600 Ok(None) => break,
2601 Err(e) => {
2602 signal_error(&shared, format!("download error: {e}"));
2603 return;
2604 }
2605 }
2606 total_drained += process_staging_lowmem(
2607 &shared,
2608 &decryptor,
2609 &mut staging,
2610 &mut remainder,
2611 &mut header_written,
2612 );
2613 if total_drained > 512 * 1024 {
2614 staging.shrink_to_fit();
2615 total_drained = 0;
2616 }
2617 }
2618 process_staging_lowmem(
2619 &shared,
2620 &decryptor,
2621 &mut staging,
2622 &mut remainder,
2623 &mut header_written,
2624 );
2625 drop(staging);
2626 if !remainder.is_empty() {
2627 append_to_buffer(&shared, &remainder);
2628 remainder.clear();
2629 remainder.shrink_to_fit();
2630 }
2631 let (lock, cvar) = &*shared;
2632 let mut state = lock.lock();
2633 state.finished = true;
2634 cvar.notify_all();
2635 debug!(
2636 "Amazon streaming: download complete ({} bytes)",
2637 state.available
2638 );
2639 }
2640 fn process_staging_lowmem(
2641 shared: &Arc<(Mutex<BufferState>, Condvar)>,
2642 decryptor: &CencDecryptor,
2643 staging: &mut Vec<u8>,
2644 remainder: &mut Vec<u8>,
2645 header_written: &mut bool,
2646 ) -> usize {
2647 let mut total_drained = 0;
2648 loop {
2649 if staging.len() < 8 {
2650 break;
2651 }
2652 let box_size =
2653 u32::from_be_bytes([staging[0], staging[1], staging[2], staging[3]]) as usize;
2654 if box_size < 8 {
2655 break;
2656 }
2657 let box_type = [staging[4], staging[5], staging[6], staging[7]];
2658 if &box_type == b"moof" {
2659 if staging.len() < box_size + 8 {
2660 break;
2661 }
2662 let mdat_size = u32::from_be_bytes([
2663 staging[box_size],
2664 staging[box_size + 1],
2665 staging[box_size + 2],
2666 staging[box_size + 3],
2667 ]) as usize;
2668 let mdat_type = [
2669 staging[box_size + 4],
2670 staging[box_size + 5],
2671 staging[box_size + 6],
2672 staging[box_size + 7],
2673 ];
2674 if &mdat_type != b"mdat" || mdat_size < 8 {
2675 staging.drain(..box_size);
2676 total_drained += box_size;
2677 continue;
2678 }
2679 let fragment_total = box_size + mdat_size;
2680 if staging.len() < fragment_total {
2681 break;
2682 }
2683 let _ = decryptor.decrypt_buffer(&mut staging[..fragment_total]);
2684 let payload_start = box_size + 8;
2685 let payload_end = box_size + mdat_size;
2686 if *header_written && payload_end > payload_start {
2687 let payload_len = payload_end - payload_start;
2688 let mut payload_copy = Vec::with_capacity(payload_len);
2689 payload_copy.extend_from_slice(&staging[payload_start..payload_end]);
2690 flush_complete_flac_frames(shared, remainder, &payload_copy);
2691 }
2692 staging.drain(..fragment_total);
2693 total_drained += fragment_total;
2694 continue;
2695 }
2696 if staging.len() < box_size {
2697 break;
2698 }
2699 if &box_type == b"moov" && !*header_written {
2700 match extract_flac_stream_header(&staging[..box_size]) {
2701 Some(hdr) => {
2702 debug!("Amazon FLAC: header extracted ({} bytes)", hdr.len());
2703 append_to_buffer(shared, &hdr);
2704 *header_written = true;
2705 }
2706 None => warn!("Amazon FLAC: failed to extract FLAC header from moov"),
2707 }
2708 }
2709 staging.drain(..box_size);
2710 total_drained += box_size;
2711 }
2712 total_drained
2713 }
2714 fn flush_complete_flac_frames(
2715 shared: &Arc<(Mutex<BufferState>, Condvar)>,
2716 remainder: &mut Vec<u8>,
2717 payload: &[u8],
2718 ) {
2719 remainder.extend_from_slice(payload);
2720 let mut pos = 0;
2721 loop {
2722 if pos >= remainder.len() {
2723 break;
2724 }
2725 match next_flac_frame_end(&remainder[pos..]) {
2726 Some(frame_len) => {
2727 append_to_buffer(shared, &remainder[pos..pos + frame_len]);
2728 pos += frame_len;
2729 }
2730 None => {
2731 break;
2732 }
2733 }
2734 }
2735 remainder.drain(..pos);
2736 }
2737 fn next_flac_frame_end(data: &[u8]) -> Option<usize> {
2738 if data.len() < 6 {
2739 return None;
2740 }
2741 if (data[0] != 0xFF) || (data[1] & 0xFE) != 0xF8 {
2742 if let Some(next) = find_sync(&data[1..]) {
2743 return next_flac_frame_end(&data[1 + next..]);
2744 }
2745 return None;
2746 }
2747 let search_from = 6;
2748 if data.len() <= search_from {
2749 return None;
2750 }
2751 find_sync(&data[search_from..]).map(|offset| search_from + offset)
2752 }
2753 fn find_sync(data: &[u8]) -> Option<usize> {
2754 data.windows(2)
2755 .position(|w| w[0] == 0xFF && (w[1] & 0xFE) == 0xF8)
2756 }
2757 fn append_to_buffer(shared: &Arc<(Mutex<BufferState>, Condvar)>, data: &[u8]) {
2758 let (lock, cvar) = &**shared;
2759 let mut state = lock.lock();
2760 state.data.extend_from_slice(data);
2761 state.available = state.data.len();
2762 cvar.notify_all();
2763 }
2764 fn signal_error(shared: &Arc<(Mutex<BufferState>, Condvar)>, msg: String) {
2765 warn!("Amazon FLAC streaming error: {}", msg);
2766 let (lock, cvar) = &**shared;
2767 let mut state = lock.lock();
2768 state.error = Some(msg);
2769 state.finished = true;
2770 cvar.notify_all();
2771 }
2772}
2773pub mod track {
2774 use crate::protocol::tracks::TrackInfo;
2775 pub struct AmazonTrackData {
2776 pub track_id: String,
2777 pub title: String,
2778 pub artist: String,
2779 pub duration_ms: u64,
2780 pub artwork_url: Option<String>,
2781 pub isrc: Option<String>,
2782 }
2783 impl AmazonTrackData {
2784 pub fn into_track_info(self) -> TrackInfo {
2785 let uri = format!("https://music.amazon.com/tracks/{}", self.track_id);
2786 TrackInfo {
2787 identifier: self.track_id,
2788 is_seekable: true,
2789 author: self.artist,
2790 length: self.duration_ms,
2791 is_stream: false,
2792 position: 0,
2793 title: self.title,
2794 uri: Some(uri),
2795 artwork_url: self.artwork_url,
2796 isrc: self.isrc,
2797 source_name: "amazonmusic".to_string(),
2798 }
2799 }
2800 }
2801}
2802pub mod validators {
2803 use serde_json::Value;
2804 pub fn is_invalid_track(resp: &Value) -> bool {
2805 let methods = match resp["methods"].as_array() {
2806 Some(m) if !m.is_empty() => m,
2807 _ => return true,
2808 };
2809 let has_error_note = methods.iter().any(|m| {
2810 m["interface"]
2811 .as_str()
2812 .map(|i| i.contains("ShowNotificationMethod"))
2813 .unwrap_or(false)
2814 && m["notification"]["message"]["text"]
2815 .as_str()
2816 .map(|t| t.contains("no longer available"))
2817 .unwrap_or(false)
2818 });
2819 let is_homepage = methods[0]["template"]["interface"]
2820 .as_str()
2821 .map(|i| i.contains("GalleryTemplate"))
2822 .unwrap_or(false)
2823 && methods[0]["template"]["widgets"]
2824 .as_array()
2825 .map(|w| w.is_empty())
2826 .unwrap_or(false);
2827 has_error_note || is_homepage
2828 }
2829 pub fn is_invalid_album(resp: &Value) -> bool {
2830 let methods = match resp["methods"].as_array() {
2831 Some(m) if !m.is_empty() => m,
2832 _ => return true,
2833 };
2834 let template = &methods[0]["template"];
2835 template["interface"]
2836 .as_str()
2837 .map(|i| i.contains("DialogTemplate"))
2838 .unwrap_or(false)
2839 && template["header"]
2840 .as_str()
2841 .map(|h| h == "Service error")
2842 .unwrap_or(false)
2843 }
2844 pub fn is_invalid_artist(resp: &Value) -> bool {
2845 let methods = match resp["methods"].as_array() {
2846 Some(m) if !m.is_empty() => m,
2847 _ => return true,
2848 };
2849 let template = &methods[0]["template"];
2850 template["interface"]
2851 .as_str()
2852 .map(|i| i.contains("MessageTemplate"))
2853 .unwrap_or(false)
2854 && template["header"]
2855 .as_str()
2856 .map(|h| h == "We're Sorry")
2857 .unwrap_or(false)
2858 && template["message"]
2859 .as_str()
2860 .map(|m| m.contains("unable to complete your action"))
2861 .unwrap_or(false)
2862 }
2863 pub fn is_invalid_playlist(resp: &Value) -> bool {
2864 let methods = match resp["methods"].as_array() {
2865 Some(m) if !m.is_empty() => m,
2866 _ => return true,
2867 };
2868 let first = &methods[0];
2869 if first["template"]["widgets"]
2870 .as_array()
2871 .map(|w| w.is_empty())
2872 .unwrap_or(false)
2873 {
2874 return true;
2875 }
2876 if let Some(second) = methods.get(1) {
2877 let msg = second["notification"]["message"]["text"]
2878 .as_str()
2879 .or_else(|| second["notification"]["message"]["innerHTML"].as_str())
2880 .unwrap_or("");
2881 if msg
2882 .to_lowercase()
2883 .contains("playlist is no longer available")
2884 {
2885 return true;
2886 }
2887 }
2888 first["template"]["templateData"]["deeplink"]
2889 .as_str()
2890 .map(|d| d == "/")
2891 .unwrap_or(false)
2892 }
2893 pub fn is_invalid_community_playlist(resp: &Value) -> bool {
2894 let template = match resp["methods"].as_array().and_then(|m| m.first()) {
2895 Some(m) => &m["template"],
2896 None => return false,
2897 };
2898 let is_dialog = template["interface"]
2899 .as_str()
2900 .map(|i| {
2901 i == "Web.TemplatesInterface.v1_0.Touch.DialogTemplateInterface.DialogTemplate"
2902 })
2903 .unwrap_or(false);
2904 let is_service_error = template["header"]
2905 .as_str()
2906 .map(|h| h.trim().to_lowercase() == "service error")
2907 .unwrap_or(false);
2908 let has_error_body = template["body"]["text"]
2909 .as_str()
2910 .map(|t| t.to_lowercase().contains("sorry something went wrong"))
2911 .unwrap_or(false);
2912 is_dialog && is_service_error && has_error_body
2913 }
2914}
2915pub use manager::AmazonMusicSource;