Skip to main content

lavende_core/sources/
jiosaavn.rs

1pub mod helpers {
2    use serde_json::Value;
3    use tracing::warn;
4    const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36";
5    pub async fn get_json(
6        client: &reqwest::Client,
7        api_url: &str,
8        params: &[(&str, &str)],
9    ) -> Option<Value> {
10        let resp = match client
11            .get(api_url)
12            .header("User-Agent", USER_AGENT)
13            .header("Accept", "application/json")
14            .header("Accept-Language", "en-US,en;q=0.9")
15            .header("Referer", "https://www.jiosaavn.com/")
16            .header("Origin", "https://www.jiosaavn.com")
17            .query(params)
18            .send()
19            .await
20        {
21            Ok(r) => r,
22            Err(e) => {
23                warn!("JioSaavn request failed: {e}");
24                return None;
25            }
26        };
27        if !resp.status().is_success() {
28            warn!("JioSaavn API error status: {}", resp.status());
29            return None;
30        }
31        let text = match resp.text().await {
32            Ok(text) => text,
33            Err(e) => {
34                warn!("Failed to read JioSaavn response body: {e}");
35                return None;
36            }
37        };
38        serde_json::from_str(&text).ok()
39    }
40    pub fn clean_string(s: &str) -> String {
41        s.replace("&quot;", "\"").replace("&amp;", "&")
42    }
43}
44pub mod metadata {
45    use super::{
46        JioSaavnSource,
47        helpers::{clean_string, get_json},
48        parser::parse_track,
49    };
50    use crate::protocol::tracks::{LoadError, LoadResult, PlaylistData, PlaylistInfo};
51    use serde_json::Value;
52    impl JioSaavnSource {
53        pub async fn fetch_metadata(&self, id: &str) -> Option<Value> {
54            let params = vec![
55                ("__call", "webapi.get"),
56                ("api_version", "4"),
57                ("_format", "json"),
58                ("_marker", "0"),
59                ("ctx", "web6dot0"),
60                ("token", id),
61                ("type", "song"),
62            ];
63            get_json(&self.client, &self.api_url, &params)
64                .await
65                .and_then(|json| {
66                    json.get("songs")
67                        .and_then(|s| s.get(0))
68                        .cloned()
69                        .or_else(|| (json.get("id").is_some()).then_some(json))
70                })
71        }
72        pub async fn resolve_list(&self, type_: &str, id: &str) -> LoadResult {
73            let t = if type_ == "featured" || type_ == "s/playlist" {
74                "playlist"
75            } else {
76                type_
77            };
78            let n = if type_ == "artist" {
79                self.artist_load_limit
80            } else if type_ == "album" {
81                self.album_load_limit
82            } else {
83                self.playlist_load_limit
84            };
85            let n_str = n.to_string();
86            let mut params = vec![
87                ("__call", "webapi.get"),
88                ("api_version", "4"),
89                ("_format", "json"),
90                ("_marker", "0"),
91                ("ctx", "web6dot0"),
92                ("token", id),
93                ("type", t),
94            ];
95            if type_ == "artist" {
96                params.push(("n_song", &n_str));
97            } else {
98                params.push(("n", &n_str));
99            }
100            if let Some(data) = get_json(&self.client, &self.api_url, &params).await {
101                let list = data
102                    .get("list")
103                    .or_else(|| data.get("topSongs"))
104                    .and_then(|v| v.as_array());
105                if let Some(arr) = list
106                    && !arr.is_empty()
107                {
108                    let tracks: Vec<_> = arr.iter().filter_map(parse_track).collect();
109                    let mut name = clean_string(
110                        data.get("title")
111                            .or_else(|| data.get("name"))
112                            .and_then(|v| v.as_str())
113                            .unwrap_or(""),
114                    );
115                    if type_ == "artist" {
116                        name = format!("{name}'s Top Tracks");
117                    }
118                    return LoadResult::Playlist(PlaylistData {
119                        info: PlaylistInfo {
120                            name,
121                            selected_track: -1,
122                        },
123                        plugin_info: serde_json::json!({
124                            "url": data.get("perma_url").and_then(|v| v.as_str()),
125                            "type": type_,
126                            "artworkUrl": data.get("image").and_then(|v| v.as_str()).map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500")),
127                            "author": data.get("subtitle").or_else(|| data.get("header_desc")).and_then(|v| v.as_str()).map(|s| s.split(',').take(3).collect::<Vec<_>>().join(", ")),
128                            "totalTracks": data.get("list_count").and_then(|v| v.as_str()).and_then(|s| s.parse::<u64>().ok()).unwrap_or(tracks.len() as u64)
129                        }),
130                        tracks,
131                    });
132                }
133                LoadResult::Empty {}
134            } else {
135                LoadResult::Error(LoadError {
136                    message: Some("JioSaavn list fetch failed".to_owned()),
137                    severity: crate::common::Severity::Common,
138                    cause: String::new(),
139                    cause_stack_trace: None,
140                })
141            }
142        }
143    }
144}
145pub mod parser {
146    use crate::protocol::tracks::{PlaylistData, PlaylistInfo, Track, TrackInfo};
147    use serde_json::Value;
148    pub fn parse_track(v: &Value) -> Option<Track> {
149        let id = v.get("id").and_then(|v| {
150            v.as_str()
151                .map(|s| s.to_owned())
152                .or_else(|| v.as_i64().map(|i| i.to_string()))
153        })?;
154        let title = super::helpers::clean_string(
155            v.get("title")
156                .or_else(|| v.get("song"))
157                .and_then(|v| v.as_str())?,
158        );
159        let duration = v
160            .pointer("/more_info/duration")
161            .or_else(|| v.get("duration"))
162            .and_then(|v| v.as_str())
163            .and_then(|s| s.parse::<u64>().ok())
164            .or_else(|| v.pointer("/more_info/duration").and_then(|v| v.as_u64()))
165            .or_else(|| v.get("duration").and_then(|v| v.as_u64()))
166            .unwrap_or(0);
167        let artwork_url = v
168            .get("image")
169            .and_then(|v| v.as_str())
170            .filter(|s| !s.is_empty())
171            .map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500"));
172        let artists_str = v
173            .pointer("/more_info/artistMap/primary_artists")
174            .and_then(|v| v.as_array())
175            .filter(|arr| !arr.is_empty())
176            .or_else(|| {
177                v.pointer("/more_info/artistMap/artists")
178                    .and_then(|v| v.as_array())
179            })
180            .map(|arr| {
181                arr.iter()
182                    .filter_map(|a| a.get("name").and_then(|v| v.as_str()))
183                    .take(3)
184                    .collect::<Vec<_>>()
185                    .join(", ")
186            })
187            .or_else(|| {
188                v.pointer("/more_info/music")
189                    .or_else(|| v.get("subtitle"))
190                    .or_else(|| v.get("primary_artists"))
191                    .or_else(|| v.get("singers"))
192                    .or_else(|| v.get("header_desc"))
193                    .and_then(|v| v.as_str())
194                    .map(|s| {
195                        s.split(',')
196                            .map(|part| part.trim())
197                            .take(3)
198                            .collect::<Vec<_>>()
199                            .join(", ")
200                    })
201            })
202            .unwrap_or_else(|| "Unknown Artist".to_owned());
203        let author = super::helpers::clean_string(&artists_str);
204        let mut track = Track::new(TrackInfo {
205            title,
206            author,
207            length: duration * 1000,
208            identifier: id,
209            source_name: "jiosaavn".to_owned(),
210            uri: v
211                .get("perma_url")
212                .and_then(|v| v.as_str())
213                .filter(|s| !s.is_empty())
214                .map(|s| s.to_owned()),
215            artwork_url,
216            is_stream: false,
217            is_seekable: true,
218            ..Default::default()
219        });
220        track.plugin_info = serde_json::json!({
221            "albumName": v.get("album").or_else(|| v.pointer("/more_info/album")).and_then(|v| v.as_str()),
222            "albumUrl": v.get("album_url").or_else(|| v.pointer("/more_info/album_url")).and_then(|v| v.as_str()),
223            "artistUrl": v.pointer("/more_info/artistMap/primary_artists/0/perma_url").and_then(|v| v.as_str()),
224            "artistArtworkUrl": v.pointer("/more_info/artistMap/primary_artists/0/image").and_then(|v| v.as_str()).map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500")),
225            "previewUrl": v.get("media_preview_url").or_else(|| v.pointer("/more_info/media_preview_url")).or_else(|| v.get("vlink")).or_else(|| v.pointer("/more_info/vlink")).and_then(|v| v.as_str()),
226            "isPreview": false
227        });
228        Some(track)
229    }
230    pub fn parse_search_item(v: &Value) -> Option<Track> {
231        let id = v.get("id").and_then(|v| v.as_str())?;
232        let title = super::helpers::clean_string(v.get("title").and_then(|v| v.as_str())?);
233        let artwork_url = v
234            .get("image")
235            .and_then(|v| v.as_str())
236            .map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500"));
237        let author_str = v
238            .get("subtitle")
239            .or_else(|| v.get("description"))
240            .and_then(|v| v.as_str())
241            .map(|s| {
242                s.split(',')
243                    .map(|part| part.trim())
244                    .take(3)
245                    .collect::<Vec<_>>()
246                    .join(", ")
247            })
248            .unwrap_or_else(|| "Unknown Artist".to_owned());
249        let author = super::helpers::clean_string(&author_str);
250        let mut track = Track::new(TrackInfo {
251            title,
252            author,
253            length: 0,
254            identifier: id.to_owned(),
255            source_name: "jiosaavn".to_owned(),
256            uri: v.get("url").and_then(|v| v.as_str()).map(|s| s.to_owned()),
257            artwork_url,
258            is_stream: false,
259            is_seekable: true,
260            ..Default::default()
261        });
262        track.plugin_info = serde_json::json!({
263            "albumName": v.get("album").and_then(|v| v.as_str()),
264            "previewUrl": v.get("vlink").or_else(|| v.pointer("/more_info/vlink")).and_then(|v| v.as_str()),
265            "isPreview": true
266        });
267        Some(track)
268    }
269    pub fn parse_search_playlist(v: &Value, type_: &str) -> Option<PlaylistData> {
270        let title = super::helpers::clean_string(v.get("title").and_then(|v| v.as_str())?);
271        let artwork_url = v
272            .get("image")
273            .and_then(|v| v.as_str())
274            .map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500"));
275        let mut url = v
276            .get("url")
277            .or_else(|| v.get("perma_url"))
278            .and_then(|v| v.as_str())
279            .unwrap_or("")
280            .to_owned();
281        if url.is_empty() {
282            url = v
283                .get("perma_url")
284                .or_else(|| v.get("permaurl"))
285                .or_else(|| v.get("token"))
286                .or_else(|| v.pointer("/more_info/perma_url"))
287                .or_else(|| v.pointer("/more_info/token"))
288                .and_then(|v| v.as_str())
289                .unwrap_or("")
290                .to_owned();
291        }
292        if url.is_empty()
293            && let Some(id) = v.get("id").and_then(|v| v.as_str())
294        {
295            if id.starts_with('/') || id.starts_with("http") {
296                url = id.to_owned();
297            } else {
298                let path_type = match type_ {
299                    "playlist" => "s/playlist",
300                    "featured" => "featured",
301                    "album" => "album",
302                    "artist" => "artist",
303                    _ => type_,
304                };
305                let slug = title
306                    .to_lowercase()
307                    .replace(|c: char| !c.is_alphanumeric() && c != ' ', "")
308                    .replace(' ', "-");
309                url = format!("/{path_type}/{slug}/{id}");
310            }
311        }
312        if !url.is_empty() && !url.starts_with("http") {
313            url = format!("https://www.jiosaavn.com{url}");
314        }
315        let total_tracks = v
316            .pointer("/more_info/song_count")
317            .or_else(|| v.pointer("/more_info/track_count"))
318            .or_else(|| v.get("song_count"))
319            .or_else(|| v.get("track_count"))
320            .and_then(|v| {
321                v.as_str()
322                    .and_then(|s| s.parse::<u64>().ok())
323                    .or_else(|| v.as_u64())
324            })
325            .unwrap_or(0);
326        let author_raw = v
327            .pointer("/more_info/artist_name")
328            .or_else(|| v.pointer("/more_info/music"))
329            .or_else(|| v.get("music"))
330            .or_else(|| v.get("subtitle"))
331            .or_else(|| v.get("description"))
332            .and_then(|v| v.as_str())
333            .map(|s| {
334                s.split(',')
335                    .map(|part| part.trim())
336                    .take(3)
337                    .collect::<Vec<_>>()
338                    .join(", ")
339            })
340            .filter(|s| !s.is_empty());
341        let final_author = if type_ == "artist" {
342            title.clone()
343        } else {
344            author_raw.unwrap_or_else(|| "Unknown Author".to_owned())
345        };
346        Some(PlaylistData {
347            info: PlaylistInfo {
348                name: title,
349                selected_track: -1,
350            },
351            plugin_info: serde_json::json!({
352                "url": url,
353                "type": type_,
354                "artworkUrl": artwork_url,
355                "author": super::helpers::clean_string(&final_author),
356                "totalTracks": total_tracks
357            }),
358            tracks: Vec::new(),
359        })
360    }
361}
362pub mod reader {
363    use crate::{
364        audio::source::{HttpSource, create_client},
365        common::types::AnyResult,
366    };
367    use std::io::{Read, Seek, SeekFrom};
368    use symphonia::core::io::MediaSource;
369    pub struct JioSaavnReader {
370        inner: HttpSource,
371    }
372    const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36";
373    impl JioSaavnReader {
374        pub async fn new(
375            url: &str,
376            local_addr: Option<std::net::IpAddr>,
377            proxy: Option<crate::config::HttpProxyConfig>,
378        ) -> AnyResult<Self> {
379            let client = create_client(USER_AGENT.to_owned(), local_addr, proxy, None)?;
380            let inner = HttpSource::new(client, url).await?;
381            Ok(Self { inner })
382        }
383    }
384    impl Read for JioSaavnReader {
385        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
386            self.inner.read(buf)
387        }
388    }
389    impl Seek for JioSaavnReader {
390        fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
391            self.inner.seek(pos)
392        }
393    }
394    impl MediaSource for JioSaavnReader {
395        fn is_seekable(&self) -> bool {
396            self.inner.is_seekable()
397        }
398        fn byte_len(&self) -> Option<u64> {
399            self.inner.byte_len()
400        }
401    }
402}
403pub mod recommendations {
404    use super::{JioSaavnSource, helpers::get_json, parser::parse_track};
405    use crate::protocol::tracks::{LoadResult, PlaylistData, PlaylistInfo};
406    use regex::Regex;
407    impl JioSaavnSource {
408        pub async fn get_recommendations(&self, query: &str) -> LoadResult {
409            let mut id = query.to_owned();
410            let id_regex = Regex::new(r"^[A-Za-z0-9_,-]+$").unwrap();
411            if !id_regex.is_match(query) {
412                if let LoadResult::Search(tracks) = self.search(query).await {
413                    if let Some(first) = tracks.first() {
414                        id = first.info.identifier.clone();
415                    } else {
416                        return LoadResult::Empty {};
417                    }
418                } else {
419                    return LoadResult::Empty {};
420                }
421            }
422            let encoded_id = format!("[\"{id}\"]");
423            let params = vec![
424                ("__call", "webradio.createEntityStation"),
425                ("api_version", "4"),
426                ("_format", "json"),
427                ("_marker", "0"),
428                ("ctx", "android"),
429                ("entity_id", &encoded_id),
430                ("entity_type", "queue"),
431            ];
432            let station_id = get_json(&self.client, &self.api_url, &params)
433                .await
434                .and_then(|json| {
435                    json.get("stationid")
436                        .and_then(|v| v.as_str())
437                        .map(|s| s.to_owned())
438                });
439            if let Some(sid) = station_id {
440                let k_limit = self.recommendations_limit.to_string();
441                let params = vec![
442                    ("__call", "webradio.getSong"),
443                    ("api_version", "4"),
444                    ("_format", "json"),
445                    ("_marker", "0"),
446                    ("ctx", "android"),
447                    ("stationid", &sid),
448                    ("k", &k_limit),
449                ];
450                if let Some(json) = get_json(&self.client, &self.api_url, &params).await
451                    && let Some(obj) = json.as_object()
452                {
453                    let tracks: Vec<_> = obj
454                        .values()
455                        .filter_map(|v| v.get("song"))
456                        .filter_map(parse_track)
457                        .collect();
458                    if !tracks.is_empty() {
459                        return LoadResult::Playlist(PlaylistData {
460                            info: PlaylistInfo {
461                                name: "JioSaavn Recommendations".to_owned(),
462                                selected_track: -1,
463                            },
464                            plugin_info: serde_json::json!({
465                              "type": "recommendations",
466                              "totalTracks": tracks.len()
467                            }),
468                            tracks,
469                        });
470                    }
471                }
472            }
473            if let Some(metadata) = self.fetch_metadata(&id).await
474                && let Some(artist_ids) =
475                    metadata.get("primary_artists_id").and_then(|v| v.as_str())
476            {
477                let params = vec![
478                    ("__call", "search.artistOtherTopSongs"),
479                    ("api_version", "4"),
480                    ("_format", "json"),
481                    ("_marker", "0"),
482                    ("ctx", "wap6dot0"),
483                    ("artist_ids", artist_ids),
484                    ("song_id", &id),
485                    ("language", "unknown"),
486                ];
487                if let Some(json) = get_json(&self.client, &self.api_url, &params).await
488                    && let Some(arr) = json.as_array()
489                {
490                    let tracks: Vec<_> = arr
491                        .iter()
492                        .take(self.recommendations_limit)
493                        .filter_map(parse_track)
494                        .collect();
495                    if !tracks.is_empty() {
496                        return LoadResult::Playlist(PlaylistData {
497                            info: PlaylistInfo {
498                                name: "JioSaavn Recommendations".to_owned(),
499                                selected_track: -1,
500                            },
501                            plugin_info: serde_json::json!({
502                              "type": "recommendations",
503                              "totalTracks": tracks.len()
504                            }),
505                            tracks,
506                        });
507                    }
508                }
509            }
510            LoadResult::Empty {}
511        }
512    }
513}
514pub mod search {
515    use super::{
516        JioSaavnSource,
517        helpers::get_json,
518        parser::{parse_search_item, parse_search_playlist, parse_track},
519    };
520    use crate::protocol::tracks::{LoadError, LoadResult, SearchResult};
521    use tracing::debug;
522    impl JioSaavnSource {
523        pub async fn search(&self, query: &str) -> LoadResult {
524            debug!("JioSaavn searching: {query}");
525            let params = vec![
526                ("__call", "search.getResults"),
527                ("api_version", "4"),
528                ("_format", "json"),
529                ("_marker", "0"),
530                ("cc", "in"),
531                ("ctx", "web6dot0"),
532                ("includeMetaTags", "1"),
533                ("q", query),
534            ];
535            if let Some(json) = get_json(&self.client, &self.api_url, &params).await {
536                if let Some(results) = json.get("results").and_then(|v| v.as_array()) {
537                    if results.is_empty() {
538                        return LoadResult::Empty {};
539                    }
540                    let tracks: Vec<_> = results
541                        .iter()
542                        .take(self.search_limit)
543                        .filter_map(parse_track)
544                        .collect();
545                    return LoadResult::Search(tracks);
546                }
547                LoadResult::Empty {}
548            } else {
549                LoadResult::Error(LoadError {
550                    message: Some("JioSaavn search failed".to_owned()),
551                    severity: crate::common::Severity::Common,
552                    cause: String::new(),
553                    cause_stack_trace: None,
554                })
555            }
556        }
557        pub async fn get_autocomplete(
558            &self,
559            query: &str,
560            types: &[String],
561        ) -> Option<SearchResult> {
562            debug!("JioSaavn get_autocomplete: {query}");
563            let params = vec![
564                ("__call", "autocomplete.get"),
565                ("api_version", "4"),
566                ("_format", "json"),
567                ("_marker", "0"),
568                ("ctx", "web6dot0"),
569                ("query", query),
570            ];
571            let json = get_json(&self.client, &self.api_url, &params).await?;
572            let mut tracks = Vec::new();
573            let mut albums = Vec::new();
574            let mut artists = Vec::new();
575            let mut playlists = Vec::new();
576            let texts = Vec::new();
577            let all_types = types.is_empty();
578            if (all_types || types.contains(&"track".to_owned()))
579                && let Some(songs) = json
580                    .get("songs")
581                    .and_then(|v| v.get("data"))
582                    .and_then(|v| v.as_array())
583            {
584                for item in songs {
585                    if let Some(track) = parse_search_item(item) {
586                        tracks.push(track);
587                    }
588                }
589            }
590            if !tracks.is_empty() {
591                let pids: Vec<String> = tracks.iter().map(|t| t.info.identifier.clone()).collect();
592                let pids_str = pids.join(",");
593                let details_params = vec![
594                    ("__call", "song.getDetails"),
595                    ("_format", "json"),
596                    ("pids", &pids_str),
597                ];
598                if let Some(details_json) =
599                    get_json(&self.client, &self.api_url, &details_params).await
600                {
601                    for track in &mut tracks {
602                        if let Some(detail) = details_json.get(&track.info.identifier) {
603                            if let Some(duration) = detail
604                                .get("duration")
605                                .and_then(|v| v.as_str())
606                                .and_then(|s| s.parse::<u64>().ok())
607                                .or_else(|| detail.get("duration").and_then(|v| v.as_u64()))
608                            {
609                                track.info.length = duration * 1000;
610                            }
611                            if let Some(perma_url) =
612                                detail.get("perma_url").and_then(|v| v.as_str())
613                            {
614                                track.info.uri = Some(perma_url.to_owned());
615                            }
616                            track.plugin_info = serde_json::json!({
617                                "albumName": detail
618                                    .get("album")
619                                    .or_else(|| detail.pointer("/more_info/album"))
620                                    .and_then(|v| v.as_str()),
621                                "albumUrl": detail
622                                    .get("album_url")
623                                    .or_else(|| detail.pointer("/more_info/album_url"))
624                                    .and_then(|v| v.as_str()),
625                                "artistUrl": detail
626                                    .pointer("/more_info/artistMap/primary_artists/0/perma_url")
627                                    .and_then(|v| v.as_str()),
628                                "artistArtworkUrl": detail
629                                    .pointer("/more_info/artistMap/primary_artists/0/image")
630                                    .and_then(|v| v.as_str())
631                                    .map(|s| s.replace("150x150", "500x500").replace("50x50", "500x500")),
632                                "previewUrl": detail
633                                    .get("media_preview_url")
634                                    .or_else(|| detail.pointer("/more_info/media_preview_url"))
635                                    .or_else(|| detail.get("vlink"))
636                                    .or_else(|| detail.pointer("/more_info/vlink"))
637                                    .and_then(|v| v.as_str()),
638                                "isPreview": false
639                            });
640                            if let Some(artists) =
641                                detail.get("primary_artists").and_then(|v| v.as_str())
642                                && !artists.is_empty()
643                            {
644                                let limited_artists = artists
645                                    .split(',')
646                                    .map(|s| s.trim())
647                                    .take(3)
648                                    .collect::<Vec<_>>()
649                                    .join(", ");
650                                track.info.author = super::helpers::clean_string(&limited_artists);
651                            }
652                            track.encoded = track.encode();
653                        }
654                    }
655                }
656            }
657            if (all_types || types.contains(&"album".to_owned()))
658                && let Some(data) = json
659                    .get("albums")
660                    .and_then(|v| v.get("data"))
661                    .and_then(|v| v.as_array())
662            {
663                for item in data {
664                    if let Some(pd) = parse_search_playlist(item, "album") {
665                        albums.push(pd);
666                    }
667                }
668            }
669            if (all_types || types.contains(&"artist".to_owned()))
670                && let Some(data) = json
671                    .get("artists")
672                    .and_then(|v| v.get("data"))
673                    .and_then(|v| v.as_array())
674            {
675                for item in data {
676                    if let Some(pd) = parse_search_playlist(item, "artist") {
677                        artists.push(pd);
678                    }
679                }
680            }
681            if (all_types || types.contains(&"playlist".to_owned()))
682                && let Some(data) = json
683                    .get("playlists")
684                    .and_then(|v| v.get("data"))
685                    .and_then(|v| v.as_array())
686            {
687                for item in data {
688                    if let Some(pd) = parse_search_playlist(item, "playlist") {
689                        playlists.push(pd);
690                    }
691                }
692            }
693            if (all_types || types.is_empty())
694                && let Some(top_data) = json
695                    .get("topquery")
696                    .and_then(|v| v.get("data"))
697                    .and_then(|v| v.as_array())
698            {
699                for item in top_data {
700                    let item_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
701                    match item_type {
702                        "song" => {
703                            if let Some(track) = parse_search_item(item)
704                                && !tracks
705                                    .iter()
706                                    .any(|t| t.info.identifier == track.info.identifier)
707                            {
708                                tracks.insert(0, track);
709                            }
710                        }
711                        "album" => {
712                            if let Some(pd) = parse_search_playlist(item, "album")
713                                && !albums.iter().any(|a| a.info.name == pd.info.name)
714                            {
715                                albums.insert(0, pd);
716                            }
717                        }
718                        "artist" => {
719                            if let Some(pd) = parse_search_playlist(item, "artist")
720                                && !artists.iter().any(|a| a.info.name == pd.info.name)
721                            {
722                                artists.insert(0, pd);
723                            }
724                        }
725                        "playlist" => {
726                            if let Some(pd) = parse_search_playlist(item, "playlist")
727                                && !playlists.iter().any(|a| a.info.name == pd.info.name)
728                            {
729                                playlists.insert(0, pd);
730                            }
731                        }
732                        _ => {}
733                    }
734                }
735            }
736            Some(SearchResult {
737                tracks,
738                albums,
739                artists,
740                playlists,
741                texts,
742                plugin: serde_json::json!({}),
743            })
744        }
745    }
746}
747pub mod track {
748    use crate::{
749        common::AudioFormat,
750        config::HttpProxyConfig,
751        sources::playable_track::{PlayableTrack, ResolvedTrack},
752    };
753    use async_trait::async_trait;
754    use base64::prelude::*;
755    use des::{
756        Des,
757        cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray},
758    };
759    use std::net::IpAddr;
760    pub struct JioSaavnTrack {
761        pub encrypted_url: String,
762        pub secret_key: Vec<u8>,
763        pub is_320: bool,
764        pub local_addr: Option<IpAddr>,
765        pub proxy: Option<HttpProxyConfig>,
766    }
767    #[async_trait]
768    impl PlayableTrack for JioSaavnTrack {
769        async fn resolve(&self) -> Result<ResolvedTrack, String> {
770            let url = self.resolve_url().ok_or_else(|| {
771                "Failed to decrypt JioSaavn URL. Check secretKey in config.toml".to_string()
772            })?;
773            let hint = format_hint_from_url(&url);
774            let reader =
775                super::reader::JioSaavnReader::new(&url, self.local_addr, self.proxy.clone())
776                    .await
777                    .map(|r| Box::new(r) as Box<dyn symphonia::core::io::MediaSource>)
778                    .map_err(|e| format!("Failed to open stream: {e}"))?;
779            Ok(ResolvedTrack::new(reader, hint))
780        }
781    }
782    impl JioSaavnTrack {
783        fn resolve_url(&self) -> Option<String> {
784            let mut url = self.decrypt_url(&self.encrypted_url)?;
785            if self.is_320 {
786                url = url.replace("_96.mp4", "_320.mp4");
787            }
788            Some(url)
789        }
790        fn decrypt_url(&self, encrypted: &str) -> Option<String> {
791            if self.secret_key.len() != 8 {
792                return None;
793            }
794            let cipher = Des::new_from_slice(&self.secret_key).ok()?;
795            let mut data = BASE64_STANDARD.decode(encrypted).ok()?;
796            for chunk in data.chunks_mut(8) {
797                if chunk.len() == 8 {
798                    cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
799                }
800            }
801            if let Some(&last_byte) = data.last() {
802                let padding = last_byte as usize;
803                if (1..=8).contains(&padding) && data.len() >= padding {
804                    data.truncate(data.len() - padding);
805                }
806            }
807            String::from_utf8(data).ok()
808        }
809    }
810    fn format_hint_from_url(url: &str) -> Option<AudioFormat> {
811        std::path::Path::new(url)
812            .extension()
813            .and_then(|s| s.to_str())
814            .map(AudioFormat::from_ext)
815            .filter(|f| *f != AudioFormat::Unknown)
816            .or(Some(AudioFormat::Mp4))
817    }
818}
819use self::track::JioSaavnTrack;
820use crate::{protocol::tracks::LoadResult, sources::playable_track::BoxedTrack};
821use async_trait::async_trait;
822use regex::Regex;
823use std::sync::{Arc, OnceLock};
824fn url_regex() -> &'static Regex {
825    static REGEX: OnceLock<Regex> = OnceLock::new();
826    REGEX.get_or_init(|| {
827        Regex::new(r"https?://(?:www\.)?(?:jiosaavn|saavn)\.com/(?:(?<type>p/album|s/featured|s/artist|s/song|album|featured|song|s/playlist|artist)/)(?:[^/]+/)+(?<id>[A-Za-z0-9_,-]+)").unwrap()
828    })
829}
830pub struct JioSaavnSource {
831    pub(crate) client: Arc<reqwest::Client>,
832    pub(crate) secret_key: Vec<u8>,
833    pub(crate) proxy: Option<crate::config::HttpProxyConfig>,
834    pub(crate) api_url: String,
835    pub(crate) search_limit: usize,
836    pub(crate) recommendations_limit: usize,
837    pub(crate) playlist_load_limit: usize,
838    pub(crate) album_load_limit: usize,
839    pub(crate) artist_load_limit: usize,
840}
841impl JioSaavnSource {
842    pub fn new(
843        config: Option<crate::config::JioSaavnConfig>,
844        client: Arc<reqwest::Client>,
845    ) -> Result<Self, String> {
846        let (
847            secret_key,
848            search_limit,
849            recommendations_limit,
850            playlist_load_limit,
851            album_load_limit,
852            artist_load_limit,
853            proxy,
854            api_url,
855        ) = if let Some(c) = config {
856            (
857                c.decryption
858                    .and_then(|d| d.secret_key)
859                    .unwrap_or_else(|| "38346591".to_owned()),
860                c.search_limit,
861                c.recommendations_limit,
862                c.playlist_load_limit,
863                c.album_load_limit,
864                c.artist_load_limit,
865                c.proxy,
866                c.api_url
867                    .unwrap_or_else(|| "https://www.jiosaavn.com/api.php".to_owned()),
868            )
869        } else {
870            (
871                "38346591".to_owned(),
872                10,
873                10,
874                50,
875                50,
876                20,
877                None,
878                "https://www.jiosaavn.com/api.php".to_owned(),
879            )
880        };
881        Ok(Self {
882            client,
883            secret_key: secret_key.into_bytes(),
884            proxy,
885            search_limit,
886            recommendations_limit,
887            playlist_load_limit,
888            album_load_limit,
889            artist_load_limit,
890            api_url,
891        })
892    }
893}
894#[async_trait]
895impl crate::sources::plugin::SourcePlugin for JioSaavnSource {
896    fn name(&self) -> &str {
897        "jiosaavn"
898    }
899    fn can_handle(&self, identifier: &str) -> bool {
900        self.search_prefixes()
901            .iter()
902            .any(|p| identifier.starts_with(p))
903            || self
904                .rec_prefixes()
905                .iter()
906                .any(|p| identifier.starts_with(p))
907            || url_regex().is_match(identifier)
908    }
909    fn search_prefixes(&self) -> Vec<&str> {
910        vec!["jssearch:"]
911    }
912    fn rec_prefixes(&self) -> Vec<&str> {
913        vec!["jsrec:"]
914    }
915    async fn load(
916        &self,
917        identifier: &str,
918        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
919    ) -> LoadResult {
920        for prefix in self.rec_prefixes() {
921            if let Some(query) = identifier.strip_prefix(prefix) {
922                return self.get_recommendations(query).await;
923            }
924        }
925        for prefix in self.search_prefixes() {
926            if let Some(query) = identifier.strip_prefix(prefix) {
927                return self.search(query).await;
928            }
929        }
930        if let Some(caps) = url_regex().captures(identifier) {
931            let type_ = caps.name("type").map(|m| m.as_str()).unwrap_or("");
932            let id = caps.name("id").map(|m| m.as_str()).unwrap_or("");
933            if id.is_empty() || type_.is_empty() {
934                return LoadResult::Empty {};
935            }
936            let canonical_type = match type_ {
937                "p/album" => "album",
938                "s/artist" => "artist",
939                "s/featured" => "featured",
940                other => other,
941            };
942            if canonical_type == "song" || canonical_type == "s/song" {
943                if let Some(track_data) = self.fetch_metadata(id).await {
944                    if let Some(track) = parser::parse_track(&track_data) {
945                        return LoadResult::Track(track);
946                    }
947                }
948                return LoadResult::Empty {};
949            } else {
950                return self.resolve_list(canonical_type, id).await;
951            }
952        }
953        LoadResult::Empty {}
954    }
955    async fn get_track(
956        &self,
957        identifier: &str,
958        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
959    ) -> Option<BoxedTrack> {
960        let id = if let Some(caps) = url_regex().captures(identifier) {
961            caps.name("id").map(|m| m.as_str()).unwrap_or(identifier)
962        } else {
963            identifier
964        };
965        let track_data = self.fetch_metadata(id).await?;
966        let encrypted_url = track_data
967            .get("more_info")
968            .and_then(|m| m.get("encrypted_media_url"))
969            .and_then(|v| v.as_str())?
970            .to_owned();
971        let is_320 = track_data
972            .get("more_info")
973            .and_then(|m| m.get("320kbps"))
974            .map(|v| v.as_str() == Some("true") || v.as_bool() == Some(true))
975            .unwrap_or(false);
976        let local_addr = routeplanner.and_then(|rp| rp.get_address());
977        Some(Arc::new(JioSaavnTrack {
978            encrypted_url,
979            secret_key: self.secret_key.clone(),
980            is_320,
981            local_addr,
982            proxy: self.proxy.clone(),
983        }))
984    }
985    fn get_proxy_config(&self) -> Option<crate::config::HttpProxyConfig> {
986        self.proxy.clone()
987    }
988    async fn load_search(
989        &self,
990        query: &str,
991        types: &[String],
992        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
993    ) -> Option<crate::protocol::tracks::SearchResult> {
994        let mut q = query;
995        for prefix in self.search_prefixes() {
996            if let Some(stripped) = query.strip_prefix(prefix) {
997                q = stripped;
998                break;
999            }
1000        }
1001        self.get_autocomplete(q, types).await
1002    }
1003}