1#[cfg(feature = "cache")]
2use std::time::Duration;
3use std::usize;
4
5use rand::{Rng, RngCore};
6use serde_json::{json, Value};
7use serde_repr::{Deserialize_repr, Serialize_repr};
8
9use crate::{
10 client::{ApiClient, ApiClientBuilder, ApiRequestBuilder, ApiResponse, API_ROUTE},
11 hex::md5_hex,
12 TResult,
13};
14
15#[derive(Default)]
17pub struct NcmApi {
18 client: ApiClient,
19}
20
21impl NcmApi {
22 pub fn new(
24 #[cfg(feature = "cache")] enable_cache: bool,
25 #[cfg(feature = "cache")] cache_exp: Duration,
26 #[cfg(feature = "cache")] cache_clean_interval: Duration,
27 preserve_cookies: bool,
28 cookie_path: &str,
29 ) -> Self {
30 Self {
31 #[cfg(feature = "cache")]
32 client: ApiClientBuilder::new(cookie_path)
33 .cookie_path(cookie_path)
34 .cache(enable_cache)
35 .cache_exp(cache_exp)
36 .cache_clean_interval(cache_clean_interval)
37 .preserve_cookies(preserve_cookies)
38 .build()
39 .unwrap(),
40 #[cfg(not(feature = "cache"))]
41 client: ApiClientBuilder::new(cookie_path)
42 .cookie_path(cookie_path)
43 .preserve_cookies(preserve_cookies)
44 .build()
45 .unwrap(),
46 }
47 }
48}
49
50impl NcmApi {
52 async fn _search(&self, key: &str, route: &str, opt: Option<Value>) -> TResult<ApiResponse> {
53 let r = ApiRequestBuilder::post(API_ROUTE[route])
54 .set_data(limit_offset(30, 0))
55 .merge(json!({
56 "s": key,
57 "type": 1,
58 }))
59 .merge(opt.unwrap_or_default())
60 .build();
61
62 self.client.request(r).await
63 }
64
65 pub async fn search(&self, key: &str, opt: Option<Value>) -> TResult<ApiResponse> {
75 self._search(key, "cloudsearch", opt).await
76 }
77
78 pub async fn album_sub(&self, id: usize, op: u8) -> TResult<ApiResponse> {
83 let op = if op == 1 { "sub" } else { "unsub" };
84 let u = replace_all_route_params(API_ROUTE["album_sub"], op);
85 let r = ApiRequestBuilder::post(&u)
86 .set_data(json!({
87 "id": id,
88 }))
89 .build();
90
91 self.client.request(r).await
92 }
93
94 pub async fn album_sublist(&self, opt: Option<Value>) -> TResult<ApiResponse> {
99 let r = ApiRequestBuilder::post(API_ROUTE["album_sublist"])
100 .set_data(limit_offset(25, 0))
101 .insert("total", Value::Bool(true))
102 .merge(opt.unwrap_or_default())
103 .build();
104
105 self.client.request(r).await
106 }
107
108 pub async fn album(&self, id: usize) -> TResult<ApiResponse> {
112 let u = replace_all_route_params(API_ROUTE["album"], &id.to_string());
113 let r = ApiRequestBuilder::post(&u).build();
114
115 self.client.request(r).await
116 }
117
118 pub async fn artist_songs(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
126 let r = ApiRequestBuilder::post(API_ROUTE["artist_songs"])
127 .set_data(json!({
128 "id": id,
129 "private_cloud": true,
130 "work_type": 1,
131 "order": "hot",
132 "offset": 0,
133 "limit": 100,
134 }))
135 .merge(opt.unwrap_or_default())
136 .add_cookie("os", "pc")
137 .build();
138
139 self.client.request(r).await
140 }
141
142 pub async fn artist_sub(&self, id: usize, sub: u8) -> TResult<ApiResponse> {
147 let mut opt = "sub";
148 if sub != 1 {
149 opt = "unsub";
150 }
151
152 let u = replace_all_route_params(API_ROUTE["artist_sub"], opt);
153 let r = ApiRequestBuilder::post(&u)
154 .set_data(json!({
155 "artistId": id,
156 "artistIds": [id]
157 }))
158 .build();
159
160 self.client.request(r).await
161 }
162
163 pub async fn artist_sublist(&self, opt: Option<Value>) -> TResult<ApiResponse> {
165 let r = ApiRequestBuilder::post(API_ROUTE["artist_sublist"])
166 .set_data(limit_offset(25, 0))
167 .merge(opt.unwrap_or_default())
168 .insert("total", Value::Bool(true))
169 .build();
170
171 self.client.request(r).await
172 }
173
174 pub async fn artist_top_song(&self, id: usize) -> TResult<ApiResponse> {
178 let r = ApiRequestBuilder::post(API_ROUTE["artist_top_song"])
179 .set_data(json!({ "id": id }))
180 .build();
181
182 self.client.request(r).await
183 }
184
185 pub async fn check_music(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
191 let r = ApiRequestBuilder::post(API_ROUTE["check_music"])
192 .set_data(json!({"br": 999000}))
193 .merge(opt.unwrap_or_default())
194 .merge(json!({ "ids": [id] }))
195 .build();
196
197 self.client.request(r).await
198 }
199
200 pub async fn comment_hot(
210 &self,
211 id: usize,
212 resouce_type: ResourceType,
213 opt: Option<Value>,
214 ) -> TResult<ApiResponse> {
215 let u = replace_all_route_params(API_ROUTE["comment_hot"], "");
216 let u = format!("{}{}{}", u, map_resource_code(resouce_type), id);
217
218 let r = ApiRequestBuilder::post(&u)
219 .add_cookie("os", "pc")
220 .set_data(limit_offset(20, 0))
221 .merge(opt.unwrap_or_default())
222 .merge(json!({
223 "beforeTime": 0,
224 "rid": id
225 }))
226 .build();
227
228 self.client.request(r).await
229 }
230
231 #[allow(clippy::too_many_arguments)]
244 pub async fn comment(
245 &self,
246 id: usize,
247 resource_type: ResourceType,
248 page_size: usize,
249 page_no: usize,
250 sort_type: usize,
251 cursor: usize,
252 show_inner: bool,
253 ) -> TResult<ApiResponse> {
254 let mut cursor = cursor;
255 if sort_type != 3 {
256 cursor = (page_no - 1) * page_size;
257 }
258
259 let r = ApiRequestBuilder::post(API_ROUTE["comment_new"])
260 .set_crypto(crate::crypto::Crypto::Eapi)
261 .add_cookie("os", "pc")
262 .set_api_url("/api/v2/resource/comments")
263 .set_data(json!({
264 "pageSize": page_size,
265 "pageNo": page_no,
266 "sortType": sort_type,
267 "cursor": cursor,
268 "showInner": show_inner,
269 }))
270 .insert(
271 "threadId",
272 Value::String(format!("{}{}", map_resource_code(resource_type), id)),
273 )
274 .build();
275
276 self.client.request(r).await
277 }
278
279 pub async fn comment_create(
284 &self,
285 rid: usize,
286 rt: ResourceType,
287 cmt: &str,
288 ) -> TResult<ApiResponse> {
289 let thread_id = format!("{}{}", map_resource_code(rt), rid);
290
291 let u = replace_all_route_params(API_ROUTE["comment"], "add");
292 let r = ApiRequestBuilder::post(&u)
293 .add_cookie("os", "pc")
294 .set_data(json!({"threadId": thread_id, "content": cmt}))
295 .build();
296
297 self.client.request(r).await
298 }
299
300 pub async fn comment_re(
306 &self,
307 rid: usize,
308 rt: ResourceType,
309 re_id: usize,
310 cmt: &str,
311 ) -> TResult<ApiResponse> {
312 let thread_id = format!("{}{}", map_resource_code(rt), rid);
313
314 let u = replace_all_route_params(API_ROUTE["comment"], "reply");
315 let r = ApiRequestBuilder::post(&u)
316 .add_cookie("os", "pc")
317 .set_data(json!({"threadId": thread_id, "content": cmt, "commentId": re_id}))
318 .build();
319
320 self.client.request(r).await
321 }
322
323 pub async fn comment_del(
328 &self,
329 rid: usize,
330 rt: ResourceType,
331 cmt_id: usize,
332 ) -> TResult<ApiResponse> {
333 let thread_id = format!("{}{}", map_resource_code(rt), rid);
334
335 let u = replace_all_route_params(API_ROUTE["comment"], "delete");
336 let r = ApiRequestBuilder::post(&u)
337 .add_cookie("os", "pc")
338 .set_data(json!({"threadId": thread_id, "commentId": cmt_id}))
339 .build();
340
341 self.client.request(r).await
342 }
343
344 pub async fn daily_signin(&self, opt: Option<Value>) -> TResult<ApiResponse> {
349 let r = ApiRequestBuilder::post(API_ROUTE["daily_signin"])
350 .set_data(json!({"type": 0}))
351 .merge(opt.unwrap_or_default())
352 .build();
353
354 self.client.request(r).await
355 }
356
357 pub async fn fm_trash(&self, id: usize) -> TResult<ApiResponse> {
362 let mut rng = rand::thread_rng();
363 let u = format!(
364 "https://music.163.com/weapi/radio/trash/add?alg=RT&songId={}&time={}",
365 id,
366 rng.gen_range(10..20)
367 );
368 let r = ApiRequestBuilder::post(&u)
369 .set_data(json!({ "songId": id }))
370 .build();
371
372 self.client.request(r).await
373 }
374
375 pub async fn like(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
383 let r = ApiRequestBuilder::post(API_ROUTE["like"])
384 .add_cookie("os", "pc")
385 .add_cookie("appver", "2.7.1.198277")
386 .set_real_ip("118.88.88.88")
387 .set_data(json!({"alg": "itembased", "time": 3, "like": true, "trackId": id}))
388 .merge(opt.unwrap_or_default())
389 .build();
390
391 self.client.request(r).await
392 }
393
394 pub async fn likelist(&self, uid: usize) -> TResult<ApiResponse> {
399 let r = ApiRequestBuilder::post(API_ROUTE["likelist"])
400 .set_data(json!({ "uid": uid }))
401 .build();
402
403 self.client.request(r).await
404 }
405
406 pub async fn login_phone(&self, phone: &str, password: &str) -> TResult<ApiResponse> {
414 let password = md5_hex(password.as_bytes());
415 let r = ApiRequestBuilder::post(API_ROUTE["login_cellphone"])
416 .add_cookie("os", "pc")
417 .add_cookie("appver", "2.9.7")
418 .set_data(json!({
419 "countrycode": "86",
420 "rememberLogin": "true",
421 "phone": phone,
422 "password": password,
423 }))
424 .build();
425
426 self.client.request(r).await
427 }
428
429 pub async fn login_refresh(&self) -> TResult<ApiResponse> {
431 let r = ApiRequestBuilder::post(API_ROUTE["login_refresh"]).build();
432
433 self.client.request(r).await
434 }
435
436 pub async fn login_status(&self) -> TResult<ApiResponse> {
438 let r = ApiRequestBuilder::post(API_ROUTE["login_status"]).build();
439
440 self.client.request(r).await
441 }
442
443 pub async fn logout(&self) -> TResult<ApiResponse> {
445 let r = ApiRequestBuilder::post(API_ROUTE["logout"]).build();
446
447 self.client.request(r).await
448 }
449
450 pub async fn lyric(&self, id: usize) -> TResult<ApiResponse> {
455 let r = ApiRequestBuilder::post(API_ROUTE["lyric"])
456 .add_cookie("os", "pc")
457 .set_data(json!({
458 "id": id,
459 "lv": -1,
460 "kv": -1,
461 "tv": -1,
462 }))
463 .build();
464
465 self.client.request(r).await
466 }
467
468 pub async fn personal_fm(&self) -> TResult<ApiResponse> {
470 let r = ApiRequestBuilder::post(API_ROUTE["personal_fm"]).build();
471
472 self.client.request(r).await
473 }
474
475 pub async fn playlist_detail(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
486 let r = ApiRequestBuilder::post(API_ROUTE["playlist_detail"])
487 .set_data(json!({"n": 100000, "s": 8, "id": id}))
488 .merge(opt.unwrap_or_default())
489 .build();
490
491 self.client.request(r).await
492 }
493
494 pub async fn playlist_tracks(
501 &self,
502 pid: usize,
503 op: u8,
504 tracks: Vec<usize>,
505 ) -> TResult<ApiResponse> {
506 let op = if op == 1 { "add" } else { "del" };
507 let r = ApiRequestBuilder::post(API_ROUTE["playlist_tracks"])
508 .add_cookie("os", "pc")
509 .set_data(json!({"op": op, "pid": pid, "trackIds": tracks, "imme": true}))
510 .build();
511
512 self.client.request(r).await
513 }
514
515 pub async fn playlist_update(
523 &self,
524 pid: usize,
525 name: &str,
526 desc: &str,
527 tags: Vec<&str>,
528 ) -> TResult<ApiResponse> {
529 let r = ApiRequestBuilder::post(API_ROUTE["playlist_update"])
530 .add_cookie("os", "pc")
531 .set_data(json!({
532 "/api/playlist/update/name": {"id": pid, "name": name},
533 "/api/playlist/desc/update": {"id": pid, "desc": desc},
534 "/api/playlist/tags/update": {"id": pid, "tags": tags.join(";")},
535 }))
536 .build();
537
538 self.client.request(r).await
539 }
540
541 pub async fn recommend_resource(&self) -> TResult<ApiResponse> {
543 let r = ApiRequestBuilder::post(API_ROUTE["recommend_resource"]).build();
544
545 self.client.request(r).await
546 }
547
548 pub async fn recommend_songs(&self) -> TResult<ApiResponse> {
550 let r = ApiRequestBuilder::post(API_ROUTE["recommend_songs"])
551 .add_cookie("os", "ios")
552 .build();
553
554 self.client.request(r).await
555 }
556
557 pub async fn scrobble(&self, id: usize, source_id: usize) -> TResult<ApiResponse> {
567 let mut rng = rand::thread_rng();
568 let r = ApiRequestBuilder::post(API_ROUTE["scrobble"])
569 .set_data(json!({
570 "logs": [{
571 "action": "play",
572 "json": {
573 "download": 0,
574 "end": "playend",
575 "id": id,
576 "sourceId": source_id,
577 "time": rng.gen_range(20..30),
578 "type": "song",
579 "wifi": 0,
580 }
581 }]
582 }))
583 .build();
584
585 self.client.request(r).await
586 }
587
588 pub async fn search_default(&self) -> TResult<ApiResponse> {
590 let r = ApiRequestBuilder::post(API_ROUTE["search_default"])
591 .set_crypto(crate::crypto::Crypto::Eapi)
592 .set_api_url("/api/search/defaultkeyword/get")
593 .build();
594
595 self.client.request(r).await
596 }
597
598 pub async fn search_hot_detail(&self) -> TResult<ApiResponse> {
600 let r = ApiRequestBuilder::post(API_ROUTE["search_hot_detail"]).build();
601
602 self.client.request(r).await
603 }
604
605 pub async fn search_hot(&self) -> TResult<ApiResponse> {
607 let r = ApiRequestBuilder::post(API_ROUTE["search_hot"])
608 .set_data(json!({"type": 1111}))
609 .set_ua(crate::client::UA::IPhone)
610 .build();
611
612 self.client.request(r).await
613 }
614
615 pub async fn search_suggest(&self, keyword: &str, opt: Option<Value>) -> TResult<ApiResponse> {
623 let mut device = "web";
624 if let Some(val) = opt {
625 if val["type"] == "mobile" {
626 device = "mobile"
627 }
628 }
629
630 let u = format!("{}{}", API_ROUTE["search_suggest"], device);
631 let r = ApiRequestBuilder::post(&u)
632 .set_data(json!({ "s": keyword }))
633 .build();
634
635 self.client.request(r).await
636 }
637
638 pub async fn simi_artist(&self, artist_id: usize) -> TResult<ApiResponse> {
643 let mut r = ApiRequestBuilder::post(API_ROUTE["simi_artist"])
644 .set_data(json!({ "artistid": artist_id }));
645 if self
646 .client
647 .cookie("MUSIC_U", self.client.base_url())
648 .is_none()
649 {
650 r = r.add_cookie("MUSIC_A", ANONYMOUS_TOKEN);
651 }
652
653 self.client.request(r.build()).await
654 }
655
656 pub async fn simi_playlist(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
661 let r = ApiRequestBuilder::post(API_ROUTE["simi_playlist"])
662 .set_data(limit_offset(50, 0))
663 .merge(opt.unwrap_or_default())
664 .insert("songid", json!(id))
665 .build();
666
667 self.client.request(r).await
668 }
669
670 pub async fn simi_song(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
675 let r = ApiRequestBuilder::post(API_ROUTE["simi_song"])
676 .set_data(limit_offset(50, 0))
677 .merge(opt.unwrap_or_default())
678 .insert("songid", json!(id))
679 .build();
680
681 self.client.request(r).await
682 }
683
684 pub async fn song_detail(&self, ids: &[usize]) -> TResult<ApiResponse> {
689 let list = ids
690 .iter()
691 .map(|id| json!({ "id": id }).to_string())
692 .collect::<Vec<_>>();
693 let r = ApiRequestBuilder::post(API_ROUTE["song_detail"])
694 .set_data(json!({ "c": list }))
695 .build();
696
697 self.client.request(r).await
698 }
699
700 pub async fn song_url(&self, ids: &Vec<usize>) -> TResult<ApiResponse> {
709 let mut rb = ApiRequestBuilder::post(API_ROUTE["song_url"])
710 .set_crypto(crate::crypto::Crypto::Eapi)
711 .add_cookie("os", "pc")
712 .set_api_url("/api/song/enhance/player/url")
713 .set_data(json!({"ids": ids, "br": 999000}));
714
715 if self
716 .client
717 .cookie("MUSIC_U", self.client.base_url())
718 .is_none()
719 {
720 let mut rng = rand::thread_rng();
721 let mut token = [0u8; 16];
722 rng.fill_bytes(&mut token);
723 rb = rb.add_cookie("_ntes_nuid", &hex::encode(token));
724 }
725
726 self.client.request(rb.build()).await
727 }
728
729 pub async fn user_account(&self) -> TResult<ApiResponse> {
731 let r = ApiRequestBuilder::post(API_ROUTE["user_account"]).build();
732 self.client.request(r).await
733 }
734
735 pub async fn user_cloud_detail(&self, ids: &Vec<usize>) -> TResult<ApiResponse> {
740 let r = ApiRequestBuilder::post(API_ROUTE["user_cloud_detail"])
741 .set_data(json!({ "songIds": ids }))
742 .build();
743 self.client.request(r).await
744 }
745
746 pub async fn user_cloud(&self, opt: Option<Value>) -> TResult<ApiResponse> {
753 let r = ApiRequestBuilder::post(API_ROUTE["user_cloud"])
754 .set_data(limit_offset(30, 0))
755 .merge(opt.unwrap_or_default())
756 .build();
757 self.client.request(r).await
758 }
759
760 pub async fn user_comment_history(
770 &self,
771 uid: usize,
772 opt: Option<Value>,
773 ) -> TResult<ApiResponse> {
774 let r = ApiRequestBuilder::post(API_ROUTE["user_comment_history"])
775 .set_data(json!({
776 "compose_reminder": true,
777 "compose_hot_comment": true,
778 "limit": 10,
779 "time": 0,
780 "user_id": uid,
781 }))
782 .merge(opt.unwrap_or_default())
783 .build();
784 self.client.request(r).await
785 }
786
787 pub async fn user_detail(&self, uid: usize) -> TResult<ApiResponse> {
792 let u = replace_all_route_params(API_ROUTE["user_detail"], &uid.to_string());
793 let r = ApiRequestBuilder::post(&u).build();
794 self.client.request(r).await
795 }
796
797 pub async fn user_dj(&self, uid: usize, opt: Option<Value>) -> TResult<ApiResponse> {
802 let u = replace_all_route_params(API_ROUTE["user_dj"], &uid.to_string());
803 let r = ApiRequestBuilder::post(&u)
804 .set_data(limit_offset(30, 0))
805 .merge(opt.unwrap_or_default())
806 .build();
807 self.client.request(r).await
808 }
809
810 pub async fn user_podcast(&self, uid: usize) -> TResult<ApiResponse> {
815 let r = ApiRequestBuilder::post(API_ROUTE["user_audio"])
816 .set_data(json!({ "userId": uid }))
817 .build();
818 self.client.request(r).await
819 }
820
821 pub async fn podcast_audio(&self, id: usize, opt: Option<Value>) -> TResult<ApiResponse> {
828 let r = ApiRequestBuilder::post(API_ROUTE["dj_program"])
829 .set_data(limit_offset(30, 0))
830 .merge(opt.unwrap_or_default())
831 .merge(json!({
832 "radioId": id,
833 "asc": false,
834 }))
835 .build();
836 self.client.request(r).await
837 }
838
839 pub async fn user_level(&self) -> TResult<ApiResponse> {
841 let r = ApiRequestBuilder::post(API_ROUTE["user_level"]).build();
842 self.client.request(r).await
843 }
844
845 pub async fn user_playlist(&self, uid: usize, opt: Option<Value>) -> TResult<ApiResponse> {
855 let r = ApiRequestBuilder::post(API_ROUTE["user_playlist"])
856 .set_data(limit_offset(30, 0))
857 .merge(opt.unwrap_or_default())
858 .merge(json!({"includeVideo": true, "uid": uid}))
859 .build();
860 self.client.request(r).await
861 }
862
863 pub async fn user_record(&self, uid: usize, opt: Option<Value>) -> TResult<ApiResponse> {
871 let r = ApiRequestBuilder::post(API_ROUTE["user_record"])
872 .set_data(json!({"type": 1, "uid": uid}))
873 .merge(opt.unwrap_or_default())
874 .build();
875 self.client.request(r).await
876 }
877
878 pub async fn user_subcount(&self) -> TResult<ApiResponse> {
881 let r = ApiRequestBuilder::post(API_ROUTE["user_subcount"]).build();
882 self.client.request(r).await
883 }
884}
885
886fn replace_all_route_params(u: &str, rep: &str) -> String {
887 let re = regex::Regex::new(r"\$\{.*\}").unwrap();
888 re.replace_all(u, rep).to_string()
889}
890
891fn limit_offset(limit: usize, offset: usize) -> Value {
892 json!({
893 "limit": limit,
894 "offset": offset
895 })
896}
897
898#[derive(Copy, Clone, Debug, PartialEq, Eq)]
900pub enum ResourceType {
901 Song = 0,
902 MV = 1,
903 Collection = 2,
904 Album = 3,
905 Podcast = 4,
906 Video = 5,
907 Moment = 6,
908}
909
910#[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone)]
912#[repr(usize)]
913pub enum SearchType {
914 Song = 1,
915 Album = 10,
916 Artist = 100,
917 Collection = 1000,
918 User = 1002,
919 MV = 1004,
920 Lyric = 1006,
921 Podcast = 1009,
922 Video = 1014,
923 All = 1018,
924}
925
926fn map_resource_code(t: ResourceType) -> String {
927 match t {
928 ResourceType::Song => String::from("R_SO_4_"),
929 ResourceType::MV => String::from("R_MV_5_"),
930 ResourceType::Collection => String::from("A_PL_0_"),
931 ResourceType::Album => String::from("R_AL_3_"),
932 ResourceType::Podcast => String::from("A_DJ_1_"),
933 ResourceType::Video => String::from("R_VI_62_"),
934 ResourceType::Moment => String::from("A_EV_2_"),
935 }
936}
937
938const ANONYMOUS_TOKEN: &str = "8aae43f148f990410b9a2af38324af24e87ab9227c9265627ddd10145db744295fcd8701dc45b1ab8985e142f491516295dd965bae848761274a577a62b0fdc54a50284d1e434dcc04ca6d1a52333c9a";
939
940#[cfg(test)]
941mod tests {
942 use serde::Deserialize;
943 use serde_json::Value;
944 use tokio::fs;
945
946 use crate::NcmApi;
947
948 const ALBUM_ID: usize = 34808483;
949 const SONG_ID: usize = 32977061;
950 const COLLECTION_ID: usize = 2484967117;
951 const ARTIST_ID: usize = 5771;
952 const USER_ID: usize = 49668844;
953
954 #[derive(Deserialize)]
955 struct Auth {
956 phone: String,
957 password: String,
958 }
959
960 #[tokio::test(flavor = "multi_thread")]
961 async fn test_search() {
962 let api = NcmApi::default();
963 let resp = api.search("mota", None).await;
964 assert!(resp.is_ok());
965 let res = resp.unwrap().deserialize_to_implict();
966 let code = res.code;
967 let res: Value = serde_json::from_value(res.result).unwrap();
968 println!("{res}");
969 assert_eq!(code, 200);
970 }
971
972 #[tokio::test(flavor = "multi_thread")]
973 async fn test_album_sub() {
974 let api = NcmApi::default();
975 let resp = api.album_sub(ALBUM_ID, 1).await;
976 assert!(resp.is_ok());
977
978 let res = resp.unwrap().deserialize_to_implict();
979 assert_eq!(res.code, 200);
980 }
981
982 #[tokio::test(flavor = "multi_thread")]
983 async fn test_album_sublist() {
984 let api = NcmApi::default();
985 let resp = api.album_sublist(None).await;
986 assert!(resp.is_ok());
987
988 let res = resp.unwrap().deserialize_to_implict();
989 assert_eq!(res.code, 200);
990 }
991
992 #[tokio::test(flavor = "multi_thread")]
993 async fn test_album() {
994 let api = NcmApi::default();
995 let resp = api.album(ALBUM_ID).await;
996 assert!(resp.is_ok());
997
998 let res = resp.unwrap().deserialize_to_implict();
999 assert_eq!(res.code, 200);
1000 }
1001
1002 #[tokio::test(flavor = "multi_thread")]
1003 async fn test_artist_songs() {
1004 let api = NcmApi::default();
1005 let resp = api.artist_songs(ARTIST_ID, None).await;
1006 assert!(resp.is_ok());
1007
1008 let res = resp.unwrap().deserialize_to_implict();
1009 assert_eq!(res.code, 200);
1010 }
1011
1012 #[tokio::test(flavor = "multi_thread")]
1013 async fn test_artist_sub() {
1014 let api = NcmApi::default();
1015 let resp = api.artist_sub(ARTIST_ID, 1).await;
1016 assert!(resp.is_ok());
1017
1018 let res = resp.unwrap().deserialize_to_implict();
1019 assert_eq!(res.code, 200);
1020 }
1021
1022 #[tokio::test(flavor = "multi_thread")]
1023 async fn test_artist_sublist() {
1024 let api = NcmApi::default();
1025 let resp = api.artist_sublist(None).await;
1026 assert!(resp.is_ok());
1027
1028 let res = resp.unwrap().deserialize_to_implict();
1029 assert_eq!(res.code, 200);
1030 }
1031
1032 #[tokio::test(flavor = "multi_thread")]
1033 async fn test_artist_top_song() {
1034 let api = NcmApi::default();
1035 let resp = api.artist_top_song(ARTIST_ID).await;
1036 assert!(resp.is_ok());
1037
1038 let res = resp.unwrap().deserialize_to_implict();
1039 assert_eq!(res.code, 200);
1040 }
1041
1042 #[tokio::test(flavor = "multi_thread")]
1043 async fn test_check_music() {
1044 let api = NcmApi::default();
1045 let resp = api.check_music(SONG_ID, None).await;
1046 assert!(resp.is_ok());
1047
1048 let res = resp.unwrap().deserialize_to_implict();
1049 assert_eq!(res.code, 200);
1050 }
1051
1052 #[tokio::test(flavor = "multi_thread")]
1053 async fn test_comment_hot() {
1054 let api = NcmApi::default();
1055 let resp = api
1056 .comment_hot(SONG_ID, crate::api::ResourceType::Song, None)
1057 .await;
1058 assert!(resp.is_ok());
1059
1060 let res = resp.unwrap().deserialize_to_implict();
1061 assert_eq!(res.code, 200);
1062 }
1063
1064 #[tokio::test(flavor = "multi_thread")]
1065 async fn test_comment() {
1066 let api = NcmApi::default();
1067 let resp = api
1068 .comment(SONG_ID, crate::api::ResourceType::Song, 1, 1, 1, 0, true)
1069 .await;
1070 assert!(resp.is_ok());
1071
1072 let res = resp.unwrap().deserialize_to_implict();
1073 assert_eq!(res.code, 200);
1074 }
1075
1076 #[tokio::test(flavor = "multi_thread")]
1077 async fn test_comment_create() {
1078 let api = NcmApi::default();
1079 let resp = api
1080 .comment_create(SONG_ID, crate::api::ResourceType::Song, "喜欢")
1081 .await;
1082 assert!(resp.is_ok());
1083
1084 let res = resp.unwrap().deserialize_to_implict();
1085 assert_eq!(res.code, 200);
1086 }
1087
1088 #[tokio::test(flavor = "multi_thread")]
1089 async fn test_comment_re() {}
1090
1091 #[tokio::test(flavor = "multi_thread")]
1092 async fn test_comment_del() {}
1093
1094 #[tokio::test(flavor = "multi_thread")]
1095 async fn test_daily_signin() {
1096 let api = NcmApi::default();
1097 let resp = api.daily_signin(None).await;
1098 assert!(resp.is_ok());
1099
1100 let res = resp.unwrap().deserialize_to_implict();
1101 assert_eq!(res.code, 200);
1102 }
1103
1104 #[tokio::test(flavor = "multi_thread")]
1105 async fn test_fm_trash() {
1106 let api = NcmApi::default();
1107 let resp = api.fm_trash(347230).await;
1108 assert!(resp.is_ok());
1109
1110 let res = resp.unwrap().deserialize_to_implict();
1111 assert_eq!(res.code, 200);
1112 }
1113
1114 #[tokio::test(flavor = "multi_thread")]
1115 async fn test_like() {
1116 let api = NcmApi::default();
1117 let resp = api.like(SONG_ID, None).await;
1118 assert!(resp.is_ok());
1119
1120 let res = resp.unwrap().deserialize_to_implict();
1121 assert_eq!(res.code, 200);
1122 }
1123
1124 #[tokio::test(flavor = "multi_thread")]
1125 async fn test_likelist() {
1126 let api = NcmApi::default();
1127 let resp = api.likelist(USER_ID).await;
1128 assert!(resp.is_ok());
1129
1130 let res = resp.unwrap().deserialize_to_implict();
1131 assert_eq!(res.code, 200);
1132 }
1133
1134 #[tokio::test(flavor = "multi_thread")]
1135 async fn test_login_phone() {
1136 let f = fs::read_to_string("test-data/auth.json")
1137 .await
1138 .expect("no auth file");
1139 let auth: Auth = serde_json::from_str(&f).unwrap();
1140
1141 let api = NcmApi::default();
1142 let resp = api.login_phone(&auth.phone, &auth.password).await;
1143 assert!(resp.is_ok());
1144
1145 let res = resp.unwrap().deserialize_to_implict();
1146 assert_eq!(res.code, 200);
1147 }
1148
1149 #[tokio::test(flavor = "multi_thread")]
1150 async fn test_login_refresh() {
1151 let api = NcmApi::default();
1152 let resp = api.login_refresh().await;
1153 assert!(resp.is_ok());
1154
1155 let res = resp.unwrap().deserialize_to_implict();
1156 assert_eq!(res.code, 200);
1157 }
1158
1159 #[tokio::test(flavor = "multi_thread")]
1160 async fn test_login_status() {
1161 let api = NcmApi::default();
1162 let resp = api.login_status().await;
1163 assert!(resp.is_ok());
1164
1165 let res = resp.unwrap();
1166 let res = res.deserialize_to_implict();
1167 assert_eq!(res.code, 200);
1168 }
1169
1170 #[tokio::test(flavor = "multi_thread")]
1171 async fn test_logout() {
1172 let api = NcmApi::default();
1173 let resp = api.logout().await;
1174 assert!(resp.is_ok());
1175
1176 let res = resp.unwrap();
1177 let res = res.deserialize_to_implict();
1178 assert_eq!(res.code, 200);
1179 }
1180
1181 #[tokio::test(flavor = "multi_thread")]
1182 async fn test_lyric() {
1183 let api = NcmApi::default();
1184 let resp = api.lyric(SONG_ID).await;
1185 assert!(resp.is_ok());
1186
1187 let res = resp.unwrap();
1188 let res = res.deserialize_to_implict();
1189 assert_eq!(res.code, 200);
1190 }
1191
1192 #[tokio::test(flavor = "multi_thread")]
1193 async fn test_personal_fm() {
1194 let api = NcmApi::default();
1195 let resp = api.personal_fm().await;
1196 assert!(resp.is_ok());
1197
1198 let res = resp.unwrap();
1199 let res = res.deserialize_to_implict();
1200 assert_eq!(res.code, 200);
1201 }
1202
1203 #[tokio::test(flavor = "multi_thread")]
1204 async fn test_playlist_detail() {
1205 let api = NcmApi::default();
1206 let resp = api.playlist_detail(COLLECTION_ID, None).await;
1207 assert!(resp.is_ok());
1208
1209 let res = resp.unwrap();
1210 let res = res.deserialize_to_implict();
1211 assert_eq!(res.code, 200);
1212 }
1213
1214 #[tokio::test(flavor = "multi_thread")]
1215 async fn test_playlist_tracks() {}
1216
1217 #[tokio::test(flavor = "multi_thread")]
1218 async fn test_playlist_update() {}
1219
1220 #[tokio::test(flavor = "multi_thread")]
1221 async fn test_recommend_resource() {
1222 let api = NcmApi::default();
1223 let resp = api.recommend_resource().await;
1224 assert!(resp.is_ok());
1225
1226 let res = resp.unwrap();
1227 let res = res.deserialize_to_implict();
1228 assert_eq!(res.code, 200);
1229 }
1230
1231 #[tokio::test(flavor = "multi_thread")]
1232 async fn test_recommend_songs() {
1233 let api = NcmApi::default();
1234 let resp = api.recommend_songs().await;
1235 assert!(resp.is_ok());
1236
1237 let res = resp.unwrap();
1238 let res = res.deserialize_to_implict();
1239 assert_eq!(res.code, 200);
1240 }
1241
1242 #[tokio::test(flavor = "multi_thread")]
1243 async fn test_scrobble() {
1244 let api = NcmApi::default();
1245 let resp = api.scrobble(29106885, COLLECTION_ID).await;
1246 assert!(resp.is_ok());
1247
1248 let res = resp.unwrap();
1249 let res = res.deserialize_to_implict();
1250 assert_eq!(res.code, 200);
1251 }
1252
1253 #[tokio::test(flavor = "multi_thread")]
1254 async fn test_search_default() {
1255 let api = NcmApi::default();
1256 let resp = api.search_default().await;
1257 assert!(resp.is_ok());
1258
1259 let res = resp.unwrap();
1260 let res = res.deserialize_to_implict();
1261 assert_eq!(res.code, 200);
1262 }
1263
1264 #[tokio::test(flavor = "multi_thread")]
1265 async fn test_search_hot_detail() {
1266 let api = NcmApi::default();
1267 let resp = api.search_hot_detail().await;
1268 assert!(resp.is_ok());
1269
1270 let res = resp.unwrap();
1271 let res = res.deserialize_to_implict();
1272 assert_eq!(res.code, 200);
1273 }
1274
1275 #[tokio::test(flavor = "multi_thread")]
1276 async fn test_search_hot() {
1277 let api = NcmApi::default();
1278 let resp = api.search_hot().await;
1279 assert!(resp.is_ok());
1280
1281 let res = resp.unwrap();
1282 let res = res.deserialize_to_implict();
1283 assert_eq!(res.code, 200);
1284 }
1285
1286 #[tokio::test(flavor = "multi_thread")]
1287 async fn test_search_suggest() {
1288 let api = NcmApi::default();
1289 let resp = api.search_suggest("mota", None).await;
1290 assert!(resp.is_ok());
1291
1292 let res = resp.unwrap();
1293 let res = res.deserialize_to_implict();
1294 assert_eq!(res.code, 200);
1295 }
1296
1297 #[tokio::test(flavor = "multi_thread")]
1298 async fn test_simi_artist() {
1299 let api = NcmApi::default();
1300 let resp = api.simi_artist(ARTIST_ID).await;
1301 assert!(resp.is_ok());
1302
1303 let res = resp.unwrap();
1304 let res = res.deserialize_to_implict();
1305 assert_eq!(res.code, 200);
1306 }
1307
1308 #[tokio::test(flavor = "multi_thread")]
1309 async fn test_simi_playlist() {
1310 let api = NcmApi::default();
1311 let resp = api.simi_playlist(SONG_ID, None).await;
1312 assert!(resp.is_ok());
1313
1314 let res = resp.unwrap();
1315 let res = res.deserialize_to_implict();
1316 assert_eq!(res.code, 200);
1317 }
1318
1319 #[tokio::test(flavor = "multi_thread")]
1320 async fn test_simi_song() {
1321 let api = NcmApi::default();
1322 let resp = api.simi_song(SONG_ID, None).await;
1323 assert!(resp.is_ok());
1324
1325 let res = resp.unwrap();
1326 let res = res.deserialize_to_implict();
1327 assert_eq!(res.code, 200);
1328 }
1329
1330 #[tokio::test(flavor = "multi_thread")]
1331 async fn test_song_detail() {
1332 let api = NcmApi::default();
1333 let resp = api.song_detail(&[SONG_ID]).await;
1334 assert!(resp.is_ok());
1335
1336 let res = resp.unwrap();
1337 let res = res.deserialize_to_implict();
1338 assert_eq!(res.code, 200);
1339 }
1340
1341 #[tokio::test(flavor = "multi_thread")]
1342 async fn test_song_url() {
1343 let api = NcmApi::default();
1344 let resp = api.song_url(&vec![SONG_ID]).await;
1345 assert!(resp.is_ok());
1346
1347 let res = resp.unwrap();
1348 let res = res.deserialize_to_implict();
1349 assert_eq!(res.code, 200);
1350 }
1351
1352 #[tokio::test(flavor = "multi_thread")]
1353 async fn test_user_account() {
1354 let api = NcmApi::default();
1355 let resp = api.user_account().await;
1356 assert!(resp.is_ok());
1357
1358 let res = resp.unwrap();
1359 let res = res.deserialize_to_implict();
1360 assert_eq!(res.code, 200);
1361 }
1362
1363 #[tokio::test(flavor = "multi_thread")]
1364 async fn test_user_cloud_detail() {
1365 }
1373
1374 #[tokio::test(flavor = "multi_thread")]
1375 async fn test_user_cloud() {
1376 let api = NcmApi::default();
1377 let resp = api.user_cloud(None).await;
1378 assert!(resp.is_ok());
1379
1380 let res = resp.unwrap();
1381 let res = res.deserialize_to_implict();
1382 assert_eq!(res.code, 200);
1383 }
1384
1385 #[tokio::test(flavor = "multi_thread")]
1386 async fn test_user_comment_history() {
1387 let api = NcmApi::default();
1388 let resp = api.user_comment_history(USER_ID, None).await;
1389 assert!(resp.is_ok());
1390
1391 let res = resp.unwrap();
1392 let res = res.deserialize_to_implict();
1393 assert_eq!(res.code, 200);
1394 }
1395
1396 #[tokio::test(flavor = "multi_thread")]
1397 async fn test_user_detail() {
1398 let api = NcmApi::default();
1399 let resp = api.user_detail(USER_ID).await;
1400 assert!(resp.is_ok());
1401
1402 let res = resp.unwrap();
1403 let res = res.deserialize_to_implict();
1404 assert_eq!(res.code, 200);
1405 }
1406
1407 #[tokio::test(flavor = "multi_thread")]
1408 async fn test_user_dj() {
1409 let api = NcmApi::default();
1410 let resp = api.user_dj(USER_ID, None).await;
1411 assert!(resp.is_ok());
1412
1413 let res = resp.unwrap();
1414 let res = res.deserialize_to_implict();
1415 assert_eq!(res.code, 200);
1416 }
1417
1418 #[tokio::test(flavor = "multi_thread")]
1419 async fn test_user_podcast() {
1420 let api = NcmApi::default();
1421 let resp = api.user_podcast(USER_ID).await;
1422 assert!(resp.is_ok());
1423
1424 let res = resp.unwrap();
1425 let res = res.deserialize_to_implict();
1426 assert_eq!(res.code, 200);
1427 }
1428
1429 #[tokio::test(flavor = "multi_thread")]
1430 async fn test_podcast_audio() {
1431 let api = NcmApi::default();
1432 let resp = api.podcast_audio(965114264, None).await;
1433 assert!(resp.is_ok());
1434
1435 let res = resp.unwrap();
1436 let res = res.deserialize_to_implict();
1437 assert_eq!(res.code, 200);
1438 }
1439
1440 #[tokio::test(flavor = "multi_thread")]
1441 async fn test_user_level() {
1442 let api = NcmApi::default();
1443 let resp = api.user_level().await;
1444 assert!(resp.is_ok());
1445
1446 let res = resp.unwrap();
1447 let res = res.deserialize_to_implict();
1448 assert_eq!(res.code, 200);
1449 }
1450
1451 #[tokio::test(flavor = "multi_thread")]
1452 async fn test_user_playlist() {
1453 let api = NcmApi::default();
1454 let resp = api.user_playlist(USER_ID, None).await;
1455 assert!(resp.is_ok());
1456
1457 let res = resp.unwrap();
1458 let res = res.deserialize_to_implict();
1459 assert_eq!(res.code, 200);
1460 }
1461
1462 #[tokio::test(flavor = "multi_thread")]
1463 async fn test_user_record() {
1464 let api = NcmApi::default();
1465 let resp = api.user_record(USER_ID, None).await;
1466 assert!(resp.is_ok());
1467
1468 let res = resp.unwrap();
1469 let res = res.deserialize_to_implict();
1470 assert_eq!(res.code, 200);
1471 }
1472
1473 #[tokio::test(flavor = "multi_thread")]
1474 async fn test_user_subcount() {
1475 let api = NcmApi::default();
1476 let resp = api.user_subcount().await;
1477 assert!(resp.is_ok());
1478
1479 let res = resp.unwrap();
1480 let res = res.deserialize_to_implict();
1481 assert_eq!(res.code, 200);
1482 }
1483}