1pub mod api {
2 use aes::{
3 Aes128,
4 cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray},
5 };
6 use md5::{Digest, Md5};
7 use serde::{Deserialize, de::DeserializeOwned};
8 use serde_json::Value;
9 use tracing::{debug, error};
10 #[derive(Debug, Deserialize)]
11 pub struct NeteaseResponse<T> {
12 pub code: i64,
13 #[serde(flatten)]
14 pub data: T,
15 }
16 #[derive(Debug, Deserialize)]
17 pub struct NeteaseArtist {
18 pub id: i64,
19 pub name: String,
20 }
21 #[derive(Debug, Deserialize)]
22 pub struct NeteaseAlbum {
23 pub id: i64,
24 pub name: String,
25 #[serde(alias = "picUrl")]
26 pub pic_url: Option<String>,
27 }
28 #[derive(Debug, Deserialize)]
29 pub struct NeteaseSong {
30 pub id: i64,
31 pub name: String,
32 #[serde(alias = "ar", alias = "artists")]
33 pub artists: Vec<NeteaseArtist>,
34 #[serde(alias = "al", alias = "album")]
35 pub album: Option<NeteaseAlbum>,
36 #[serde(alias = "dt", alias = "duration")]
37 pub duration: u64,
38 }
39 #[derive(Debug, Deserialize)]
40 pub struct SearchResultData {
41 pub result: SearchResultInner,
42 }
43 #[derive(Debug, Deserialize)]
44 pub struct SearchResultInner {
45 #[serde(default)]
46 pub songs: Vec<NeteaseSong>,
47 #[serde(default)]
48 pub albums: Vec<NeteaseAlbum>,
49 #[serde(default)]
50 pub artists: Vec<NeteaseArtist>,
51 #[serde(default)]
52 pub playlists: Vec<NeteasePlaylist>,
53 }
54 #[derive(Debug, Deserialize)]
55 pub struct NeteasePlaylist {
56 pub id: i64,
57 pub name: String,
58 #[serde(alias = "coverImgUrl")]
59 pub cover_img_url: Option<String>,
60 pub creator: Option<NeteaseCreator>,
61 #[serde(alias = "trackCount")]
62 pub track_count: Option<u64>,
63 }
64 #[derive(Debug, Deserialize)]
65 pub struct NeteaseCreator {
66 pub nickname: String,
67 }
68 #[derive(Debug, Deserialize)]
69 pub struct SongDetailData {
70 pub songs: Vec<NeteaseSong>,
71 }
72 #[derive(Debug, Deserialize)]
73 pub struct TrackUrlData {
74 pub data: Vec<TrackUrlItem>,
75 }
76 #[derive(Debug, Deserialize)]
77 pub struct TrackUrlItem {
78 pub id: i64,
79 pub url: Option<String>,
80 pub br: i64,
81 pub code: i64,
82 #[serde(rename = "freeTrialInfo")]
83 pub free_trial_info: Option<serde_json::Value>,
84 }
85 #[derive(Debug, Deserialize)]
86 pub struct SimilarSongsData {
87 pub songs: Vec<NeteaseSong>,
88 }
89 #[derive(Debug, Deserialize)]
90 pub struct AlbumDetailData {
91 pub album: NeteaseAlbum,
92 pub songs: Vec<NeteaseSong>,
93 }
94 #[derive(Debug, Deserialize)]
95 pub struct PlaylistDetailData {
96 pub playlist: PlaylistInfo,
97 }
98 #[derive(Debug, Deserialize)]
99 pub struct PlaylistInfo {
100 pub id: i64,
101 pub name: String,
102 pub tracks: Vec<NeteaseSong>,
103 }
104 #[derive(Debug, Deserialize)]
105 pub struct ArtistDetailData {
106 pub artist: NeteaseArtist,
107 #[serde(rename = "hotSongs")]
108 pub hot_songs: Vec<NeteaseSong>,
109 }
110 const EAPI_KEY: &[u8] = b"e82ckenh8dichen8";
111 const EAPI_URLS: &[&str] = &[
112 "https://interface3.music.163.com/eapi",
113 "https://interface.music.163.com/eapi",
114 ];
115 pub fn aes_encrypt_ecb(data: &[u8], key: &[u8]) -> Vec<u8> {
116 let cipher = Aes128::new(GenericArray::from_slice(key));
117 let mut padded_data = pkcs7_pad(data, 16);
118 for chunk in padded_data.chunks_mut(16) {
119 let block = GenericArray::from_mut_slice(chunk);
120 cipher.encrypt_block(block);
121 }
122 padded_data
123 }
124 fn pkcs7_pad(data: &[u8], block_size: usize) -> Vec<u8> {
125 let padding_len = block_size - (data.len() % block_size);
126 let mut padded = data.to_vec();
127 padded.extend(std::iter::repeat_n(padding_len as u8, padding_len));
128 padded
129 }
130 pub fn md5_hex(data: &str) -> String {
131 let mut hasher = Md5::new();
132 hasher.update(data.as_bytes());
133 let result = hasher.finalize();
134 hex::encode(result)
135 }
136 pub fn eapi_encrypt(url: &str, obj: &Value) -> String {
137 let text = serde_json::to_string(obj).unwrap_or_default();
138 let message = format!("nobody{}use{}md5forencrypt", url, text);
139 let digest = md5_hex(&message);
140 let data = format!("{}-36cd479b6b5-{}-36cd479b6b5-{}", url, text, digest);
141 let encrypted = aes_encrypt_ecb(data.as_bytes(), EAPI_KEY);
142 hex::encode(encrypted).to_uppercase()
143 }
144 pub async fn get_eapi_json<T: DeserializeOwned>(
145 client: &reqwest::Client,
146 path: &str,
147 obj: Value,
148 nuid: &str,
149 device_id: &str,
150 ) -> Option<T> {
151 let params = eapi_encrypt(path, &obj);
152 let network_path = path.strip_prefix("/api").unwrap_or(path);
153 for &base_url in EAPI_URLS {
154 let url = format!("{}{}", base_url, network_path);
155 let resp = client.post(&url)
156 .header("User-Agent", "NeteaseMusic/2.5.1 (iPhone; iOS 16.6; Scale/3.00)")
157 .header("Referer", "https://music.163.com/")
158 .header("Origin", "https://music.163.com")
159 .header("X-Real-IP", "118.88.88.88")
160 .header("X-Forwarded-For", "118.88.88.88")
161 .header("X-Netease-PC-IP", "118.88.88.88")
162 .header("Cookie", format!(
163 "os=iOS; appver=2.5.1; _ntes_nuid={}; deviceId={}; channel=AppStore; mobilename=iPhone15,3",
164 nuid, device_id
165 ))
166 .form(&[("params", params.clone())])
167 .send()
168 .await;
169 match resp {
170 Ok(r) if r.status().is_success() => {
171 let text = r.text().await.ok()?;
172 match serde_json::from_str::<NeteaseResponse<T>>(&text) {
173 Ok(res) => {
174 if res.code == 200 || res.code == 0 {
175 return Some(res.data);
176 } else {
177 debug!(
178 "Netease API {} returned application code {}: {}",
179 path, res.code, text
180 );
181 }
182 }
183 Err(e) => {
184 error!(
185 "Netease API {} failed to parse JSON: {}. Text: {}",
186 path, e, text
187 );
188 }
189 }
190 }
191 _ => continue,
192 }
193 }
194 None
195 }
196}
197pub mod manager {
198 use super::api::*;
199 use crate::protocol::tracks::{
200 LoadResult, PlaylistData, PlaylistInfo, SearchResult, Track, TrackInfo,
201 };
202 use serde_json::json;
203 pub fn parse_track(song: &NeteaseSong) -> Option<Track> {
204 let id = song.id.to_string();
205 let artist = song
206 .artists
207 .first()
208 .map(|a| a.name.as_str())
209 .unwrap_or("Unknown Artist");
210 let artwork_url = song.album.as_ref().and_then(|al| al.pic_url.clone());
211 Some(Track::new(TrackInfo {
212 identifier: id.clone(),
213 is_seekable: true,
214 author: artist.to_string(),
215 length: song.duration,
216 is_stream: false,
217 position: 0,
218 title: song.name.clone(),
219 uri: Some(format!("https://music.163.com/song?id={}", id)),
220 artwork_url,
221 isrc: None,
222 source_name: "netease".to_string(),
223 }))
224 }
225 pub fn parse_album(album: &NeteaseAlbum) -> PlaylistData {
226 PlaylistData {
227 info: PlaylistInfo {
228 name: album.name.clone(),
229 selected_track: -1,
230 },
231 plugin_info: json!({
232 "type": "album",
233 "url": format!("https://music.163.com/album?id={}", album.id),
234 "artworkUrl": album.pic_url,
235 "author": "Netease Music",
236 "totalTracks": 0
237 }),
238 tracks: Vec::new(),
239 }
240 }
241 pub fn parse_artist(artist: &NeteaseArtist) -> PlaylistData {
242 PlaylistData {
243 info: PlaylistInfo {
244 name: format!("{}'s Top Tracks", artist.name),
245 selected_track: -1,
246 },
247 plugin_info: json!({
248 "type": "artist",
249 "url": format!("https://music.163.com/artist?id={}", artist.id),
250 "artworkUrl": None::<String>,
251 "author": artist.name,
252 "totalTracks": 0
253 }),
254 tracks: Vec::new(),
255 }
256 }
257 pub fn parse_playlist(playlist: &NeteasePlaylist) -> PlaylistData {
258 PlaylistData {
259 info: PlaylistInfo {
260 name: playlist.name.clone(),
261 selected_track: -1,
262 },
263 plugin_info: json!({
264 "type": "playlist",
265 "url": format!("https://music.163.com/playlist?id={}", playlist.id),
266 "artworkUrl": playlist.cover_img_url,
267 "author": playlist.creator.as_ref().map(|c| c.nickname.clone()).unwrap_or_else(|| "Netease Music".to_string()),
268 "totalTracks": playlist.track_count.unwrap_or(0)
269 }),
270 tracks: Vec::new(),
271 }
272 }
273 #[derive(Debug, PartialEq)]
274 pub enum TrackUrlResult {
275 Success(String),
276 Code(i64),
277 Trial,
278 None,
279 }
280 pub async fn fetch_track_detail(
281 client: &reqwest::Client,
282 nuid: &str,
283 device_id: &str,
284 id: &str,
285 ) -> Option<SongDetailData> {
286 get_eapi_json(
287 client,
288 "/api/v3/song/detail",
289 json!({
290 "c": serde_json::to_string(&[json!({"id": id})]).unwrap(),
291 "header": {
292 "os": "iOS",
293 "appver": "2.5.1",
294 "deviceId": device_id
295 }
296 }),
297 nuid,
298 device_id,
299 )
300 .await
301 }
302 pub async fn fetch_album(
303 client: &reqwest::Client,
304 nuid: &str,
305 device_id: &str,
306 id: &str,
307 ) -> LoadResult {
308 match get_eapi_json::<AlbumDetailData>(
309 client,
310 &format!("/api/v1/album/{}", id),
311 json!({}),
312 nuid,
313 device_id,
314 )
315 .await
316 {
317 Some(resp) if !resp.songs.is_empty() => {
318 let tracks = resp.songs.iter().filter_map(parse_track).collect();
319 LoadResult::Playlist(PlaylistData {
320 info: PlaylistInfo {
321 name: resp.album.name,
322 selected_track: -1,
323 },
324 plugin_info: json!({ "type": "album", "id": id }),
325 tracks,
326 })
327 }
328 _ => LoadResult::Empty {},
329 }
330 }
331 pub async fn fetch_playlist(
332 client: &reqwest::Client,
333 nuid: &str,
334 device_id: &str,
335 id: &str,
336 ) -> LoadResult {
337 match get_eapi_json::<PlaylistDetailData>(
338 client,
339 "/api/v6/playlist/detail",
340 json!({ "id": id, "n": 1000 }),
341 nuid,
342 device_id,
343 )
344 .await
345 {
346 Some(resp) if !resp.playlist.tracks.is_empty() => {
347 let tracks = resp
348 .playlist
349 .tracks
350 .iter()
351 .filter_map(parse_track)
352 .collect();
353 LoadResult::Playlist(PlaylistData {
354 info: PlaylistInfo {
355 name: resp.playlist.name,
356 selected_track: -1,
357 },
358 plugin_info: json!({ "type": "playlist", "id": id }),
359 tracks,
360 })
361 }
362 _ => LoadResult::Empty {},
363 }
364 }
365 pub async fn fetch_artist(
366 client: &reqwest::Client,
367 nuid: &str,
368 device_id: &str,
369 id: &str,
370 ) -> LoadResult {
371 match get_eapi_json::<ArtistDetailData>(
372 client,
373 &format!("/api/v1/artist/{}", id),
374 json!({}),
375 nuid,
376 device_id,
377 )
378 .await
379 {
380 Some(resp) if !resp.hot_songs.is_empty() => {
381 let tracks = resp.hot_songs.iter().filter_map(parse_track).collect();
382 LoadResult::Playlist(PlaylistData {
383 info: PlaylistInfo {
384 name: format!("{}'s Top Tracks", resp.artist.name),
385 selected_track: -1,
386 },
387 plugin_info: json!({ "type": "artist", "id": id }),
388 tracks,
389 })
390 }
391 _ => LoadResult::Empty {},
392 }
393 }
394 pub async fn fetch_track_url(
395 client: &reqwest::Client,
396 nuid: &str,
397 device_id: &str,
398 id: &str,
399 level: &str,
400 encode_type: &str,
401 ) -> TrackUrlResult {
402 let resp: Option<TrackUrlData> = get_eapi_json(
403 client,
404 "/api/song/enhance/player/url/v1",
405 json!({
406 "ids": [id],
407 "level": level,
408 "encodeType": encode_type,
409 "header": {
410 "os": "iOS",
411 "appver": "2.5.1",
412 "deviceId": device_id
413 }
414 }),
415 nuid,
416 device_id,
417 )
418 .await;
419 let data_wrap = match resp {
420 Some(v) => v,
421 None => return TrackUrlResult::None,
422 };
423 let data = match data_wrap.data.first() {
424 Some(d) => d,
425 None => return TrackUrlResult::None,
426 };
427 if data.code == -110 {
428 return TrackUrlResult::Code(-110);
429 }
430 if let Some(trial_info) = &data.free_trial_info
431 && trial_info.as_object().is_some_and(|o| !o.is_empty())
432 {
433 return TrackUrlResult::Trial;
434 }
435 if let Some(url) = &data.url
436 && !url.is_empty()
437 {
438 return TrackUrlResult::Success(url.clone());
439 }
440 if data.code != 200 && data.code != 0 {
441 TrackUrlResult::Code(data.code)
442 } else {
443 TrackUrlResult::None
444 }
445 }
446 pub async fn fetch_track_url_legacy(
447 client: &reqwest::Client,
448 device_id: &str,
449 id: &str,
450 br: &str,
451 ) -> Option<String> {
452 let url = format!(
453 "https://music.163.com/api/song/enhance/player/url?id={}&ids=[{}]&br={}",
454 id, id, br
455 );
456 let resp = client.get(&url)
457 .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36")
458 .header("Referer", "https://music.163.com/")
459 .header("X-Real-IP", "118.88.88.88")
460 .header("Cookie", format!("os=pc; deviceId={}", device_id))
461 .send()
462 .await
463 .ok()?;
464 if !resp.status().is_success() {
465 return None;
466 }
467 let res: NeteaseResponse<TrackUrlData> = resp.json().await.ok()?;
468 let data = res.data.data.first()?;
469 if let Some(url) = &data.url
470 && !url.is_empty()
471 {
472 return Some(url.clone());
473 }
474 None
475 }
476 pub async fn check_url(client: &reqwest::Client, url: &str) -> bool {
477 let resp = match client.head(url)
478 .header("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/2.5.1")
479 .header("Referer", "https://music.163.com/")
480 .send()
481 .await
482 {
483 Ok(r) => r,
484 Err(_) => return false,
485 };
486 if !resp.status().is_success() {
487 return false;
488 }
489 let content_length = resp
490 .headers()
491 .get(reqwest::header::CONTENT_LENGTH)
492 .and_then(|h| h.to_str().ok())
493 .and_then(|s| s.parse::<u64>().ok())
494 .unwrap_or(0);
495 content_length >= 100 * 1024
496 }
497 pub async fn search_tracks(
498 client: &reqwest::Client,
499 nuid: &str,
500 device_id: &str,
501 query: &str,
502 limit: usize,
503 ) -> LoadResult {
504 match get_eapi_json::<SearchResultData>(
505 client,
506 "/api/cloudsearch/pc",
507 json!({
508 "s": query,
509 "type": 1,
510 "limit": limit,
511 "offset": 0,
512 "total": true
513 }),
514 nuid,
515 device_id,
516 )
517 .await
518 {
519 Some(resp) if !resp.result.songs.is_empty() => {
520 let tracks = resp.result.songs.iter().filter_map(parse_track).collect();
521 LoadResult::Search(tracks)
522 }
523 _ => LoadResult::Empty {},
524 }
525 }
526 pub async fn search_full(
527 client: &reqwest::Client,
528 nuid: &str,
529 device_id: &str,
530 query: &str,
531 types: &[String],
532 limit: usize,
533 ) -> Option<SearchResult> {
534 let mut tracks = Vec::new();
535 let mut albums = Vec::new();
536 let mut artists = Vec::new();
537 let mut playlists = Vec::new();
538 let all_types = types.is_empty();
539 if (all_types || types.contains(&"track".to_owned()))
540 && let Some(res) = fetch_search(client, nuid, device_id, query, 1, limit).await
541 {
542 tracks = res.songs.iter().filter_map(parse_track).collect();
543 }
544 if (all_types || types.contains(&"album".to_owned()))
545 && let Some(res) = fetch_search(client, nuid, device_id, query, 10, limit).await
546 {
547 albums = res.albums.iter().map(parse_album).collect();
548 }
549 if (all_types || types.contains(&"artist".to_owned()))
550 && let Some(res) = fetch_search(client, nuid, device_id, query, 100, limit).await
551 {
552 artists = res.artists.iter().map(parse_artist).collect();
553 }
554 if (all_types || types.contains(&"playlist".to_owned()))
555 && let Some(res) = fetch_search(client, nuid, device_id, query, 1000, limit).await
556 {
557 playlists = res.playlists.iter().map(parse_playlist).collect();
558 }
559 Some(SearchResult {
560 tracks,
561 albums,
562 artists,
563 playlists,
564 texts: Vec::new(),
565 plugin: json!({}),
566 })
567 }
568 async fn fetch_search(
569 client: &reqwest::Client,
570 nuid: &str,
571 device_id: &str,
572 query: &str,
573 type_id: i32,
574 limit: usize,
575 ) -> Option<SearchResultInner> {
576 get_eapi_json::<SearchResultData>(
577 client,
578 "/api/cloudsearch/pc",
579 json!({
580 "s": query,
581 "type": type_id,
582 "limit": limit,
583 "offset": 0,
584 "total": true
585 }),
586 nuid,
587 device_id,
588 )
589 .await
590 .map(|d| d.result)
591 }
592 pub async fn fetch_recommendations(
593 client: &reqwest::Client,
594 nuid: &str,
595 device_id: &str,
596 identifier: &str,
597 ) -> LoadResult {
598 let id = identifier.trim();
599 if !id.chars().all(|c| c.is_ascii_digit()) || id.is_empty() {
600 return LoadResult::Empty {};
601 }
602 match get_eapi_json::<SimilarSongsData>(
603 client,
604 "/api/v1/discovery/simiSong",
605 json!({ "songid": id }),
606 nuid,
607 device_id,
608 )
609 .await
610 {
611 Some(resp) if !resp.songs.is_empty() => {
612 let tracks = resp.songs.iter().filter_map(parse_track).collect();
613 LoadResult::Playlist(PlaylistData {
614 info: PlaylistInfo {
615 name: format!("Similar to Track {}", id),
616 selected_track: -1,
617 },
618 plugin_info: json!({ "type": "recommendations", "seed_id": id }),
619 tracks,
620 })
621 }
622 _ => LoadResult::Empty {},
623 }
624 }
625}
626pub mod track {
627 use crate::{
628 config::HttpProxyConfig,
629 sources::{
630 http::HttpTrack,
631 playable_track::{PlayableTrack, ResolvedTrack},
632 },
633 };
634 use async_trait::async_trait;
635 use std::net::IpAddr;
636 use tracing::debug;
637 pub struct NeteaseTrack {
638 pub stream_url: String,
639 pub local_addr: Option<IpAddr>,
640 pub proxy: Option<HttpProxyConfig>,
641 }
642 #[async_trait]
643 impl PlayableTrack for NeteaseTrack {
644 async fn resolve(&self) -> Result<ResolvedTrack, String> {
645 let url = self.stream_url.clone();
646 debug!("Netease playback URL: {url}");
647 HttpTrack {
648 url,
649 local_addr: self.local_addr,
650 proxy: None,
651 }
652 .resolve()
653 .await
654 }
655 }
656}
657use crate::{
658 protocol::tracks::{LoadResult, SearchResult},
659 sources::{SourcePlugin, playable_track::BoxedTrack},
660};
661use async_trait::async_trait;
662use rand::Rng;
663use regex::Regex;
664use std::sync::{Arc, OnceLock};
665use tracing::debug;
666fn url_regex() -> &'static Regex {
667 static REGEX: OnceLock<Regex> = OnceLock::new();
668 REGEX.get_or_init(|| {
669 Regex::new(r"https?://music\.163\.com/(?:(?:#|m)/)?(?P<type>song|album|playlist|artist)(?:\?id=|\/)(?P<id>\d+)").unwrap()
670 })
671}
672pub struct NeteaseSource {
673 pub(crate) client: Arc<reqwest::Client>,
674 pub(crate) proxy: Option<crate::config::HttpProxyConfig>,
675 pub(crate) search_limit: usize,
676 pub(crate) nuid: String,
677 pub(crate) device_id: String,
678}
679impl NeteaseSource {
680 pub fn new(
681 config: Option<crate::config::NeteaseMusicConfig>,
682 client: Arc<reqwest::Client>,
683 ) -> Result<Self, String> {
684 let cfg = config.ok_or("Netease Music configuration is missing")?;
685 let mut rng = rand::thread_rng();
686 let nuid: String = (0..16)
687 .map(|_| format!("{:02x}", rng.r#gen::<u8>()))
688 .collect::<Vec<String>>()
689 .join("");
690 let device_id: String = (0..8)
691 .map(|_| format!("{:02X}", rng.r#gen::<u8>()))
692 .collect::<Vec<String>>()
693 .join("");
694 Ok(Self {
695 client,
696 proxy: cfg.proxy,
697 search_limit: cfg.search_limit,
698 nuid,
699 device_id,
700 })
701 }
702}
703#[async_trait]
704impl SourcePlugin for NeteaseSource {
705 fn name(&self) -> &str {
706 "netease"
707 }
708 fn can_handle(&self, identifier: &str) -> bool {
709 self.search_prefixes()
710 .iter()
711 .any(|p| identifier.starts_with(p))
712 || self
713 .rec_prefixes()
714 .iter()
715 .any(|p| identifier.starts_with(p))
716 || url_regex().is_match(identifier)
717 }
718 fn search_prefixes(&self) -> Vec<&str> {
719 vec!["nmsearch:", "ncsearch:"]
720 }
721 fn rec_prefixes(&self) -> Vec<&str> {
722 vec!["nmrec:", "ncrec:"]
723 }
724 async fn load(
725 &self,
726 identifier: &str,
727 _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
728 ) -> LoadResult {
729 for prefix in self.search_prefixes() {
730 if let Some(query) = identifier.strip_prefix(prefix) {
731 return manager::search_tracks(
732 &self.client,
733 &self.nuid,
734 &self.device_id,
735 query,
736 self.search_limit,
737 )
738 .await;
739 }
740 }
741 for prefix in self.rec_prefixes() {
742 if let Some(query) = identifier.strip_prefix(prefix) {
743 return manager::fetch_recommendations(
744 &self.client,
745 &self.nuid,
746 &self.device_id,
747 query,
748 )
749 .await;
750 }
751 }
752 if let Some(caps) = url_regex().captures(identifier) {
753 let type_ = caps.name("type").map(|m| m.as_str()).unwrap_or("");
754 let id = caps.name("id").map(|m| m.as_str()).unwrap_or("");
755 match type_ {
756 "song" => {
757 if let Some(detail) =
758 manager::fetch_track_detail(&self.client, &self.nuid, &self.device_id, id)
759 .await
760 && let Some(song) = detail.songs.first()
761 && let Some(track) = manager::parse_track(song)
762 {
763 return LoadResult::Track(track);
764 }
765 }
766 "album" => {
767 return manager::fetch_album(&self.client, &self.nuid, &self.device_id, id)
768 .await;
769 }
770 "playlist" => {
771 return manager::fetch_playlist(&self.client, &self.nuid, &self.device_id, id)
772 .await;
773 }
774 "artist" => {
775 return manager::fetch_artist(&self.client, &self.nuid, &self.device_id, id)
776 .await;
777 }
778 _ => {}
779 }
780 return LoadResult::Empty {};
781 }
782 if identifier.chars().all(|c| c.is_ascii_digit())
783 && !identifier.is_empty()
784 && let Some(detail) =
785 manager::fetch_track_detail(&self.client, &self.nuid, &self.device_id, identifier)
786 .await
787 && let Some(song) = detail.songs.first()
788 && let Some(track) = manager::parse_track(song)
789 {
790 return LoadResult::Track(track);
791 }
792 manager::search_tracks(
793 &self.client,
794 &self.nuid,
795 &self.device_id,
796 identifier,
797 self.search_limit,
798 )
799 .await
800 }
801 async fn load_search(
802 &self,
803 query: &str,
804 _types: &[String],
805 _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
806 ) -> Option<SearchResult> {
807 let mut q = query;
808 for prefix in self.search_prefixes() {
809 if let Some(stripped) = query.strip_prefix(prefix) {
810 q = stripped;
811 break;
812 }
813 }
814 manager::search_full(
815 &self.client,
816 &self.nuid,
817 &self.device_id,
818 q,
819 _types,
820 self.search_limit,
821 )
822 .await
823 }
824 async fn get_track(
825 &self,
826 identifier: &str,
827 routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
828 ) -> Option<BoxedTrack> {
829 let id = url_regex()
830 .captures(identifier)
831 .and_then(|caps| caps.name("id"))
832 .map(|m| m.as_str())
833 .unwrap_or(identifier);
834 debug!("Netease: Resolving track ID: {}", id);
835 let mut stream_url = None;
836 let mut fallback_early = false;
837 let qualities = [
838 ("aac", "standard"),
839 ("aac", "higher"),
840 ("aac", "exhigh"),
841 ("aac", "lossless"),
842 ("aac", "hires"),
843 ("aac", "jymaster"),
844 ("aac", "sky"),
845 ("aac", "jyeffect"),
846 ("aac", "jylive"),
847 ("mp3", "standard"),
848 ("mp3", "higher"),
849 ("mp3", "exhigh"),
850 ("mp3", "lossless"),
851 ("mp3", "hires"),
852 ("mp3", "jymaster"),
853 ("mp3", "sky"),
854 ("mp3", "jyeffect"),
855 ("mp3", "jylive"),
856 ];
857 let mut first_code: Option<i64> = None;
858 for (format, level) in qualities {
859 match manager::fetch_track_url(
860 &self.client,
861 &self.nuid,
862 &self.device_id,
863 id,
864 level,
865 format,
866 )
867 .await
868 {
869 manager::TrackUrlResult::Success(url)
870 if manager::check_url(&self.client, &url).await =>
871 {
872 stream_url = Some(url);
873 break;
874 }
875 manager::TrackUrlResult::Code(-110) => {
876 fallback_early = true;
877 break;
878 }
879 manager::TrackUrlResult::Trial => {
880 debug!("Netease: Track {} is trial-only, skipping quality loop", id);
881 fallback_early = true;
882 break;
883 }
884 manager::TrackUrlResult::Code(c) => {
885 first_code = first_code.or(Some(c));
886 continue;
887 }
888 _ => continue,
889 }
890 }
891 if stream_url.is_none() || fallback_early {
892 for br in ["320000", "128000"] {
893 if let Some(url) =
894 manager::fetch_track_url_legacy(&self.client, &self.device_id, id, br).await
895 && !url.is_empty()
896 && manager::check_url(&self.client, &url).await
897 {
898 stream_url = Some(url);
899 break;
900 }
901 }
902 }
903 if stream_url.is_none() {
904 debug!(
905 "Netease: Failed to resolve playback URL for track ID: {}",
906 id
907 );
908 }
909 stream_url.map(|url| {
910 Arc::new(track::NeteaseTrack {
911 stream_url: url,
912 proxy: self.proxy.clone(),
913 local_addr: routeplanner.and_then(|rp| rp.get_address()),
914 }) as BoxedTrack
915 })
916 }
917 fn get_proxy_config(&self) -> Option<crate::config::HttpProxyConfig> {
918 self.proxy.clone()
919 }
920}