Skip to main content

lavende_core/sources/
amazonmusic.rs

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;