ncmapi2/
api.rs

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/// API wrapper.
16#[derive(Default)]
17pub struct NcmApi {
18    client: ApiClient,
19}
20
21impl NcmApi {
22    /// NecmApi constructor
23    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
50/// apis
51impl 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    /// 说明 : 调用此接口 , 传入搜索关键词可以搜索该音乐 / 专辑 / 歌手 / 歌单 / 用户 , 关键词可以多个 , 以空格隔开 ,
66    /// 如 " 周杰伦 搁浅 "( 不需要登录 ), 搜索获取的 mp3url 不能直接用 , 可通过 /song/url 接口传入歌曲 id 获取具体的播放链接
67    ///
68    /// required
69    /// 必选参数 : key: 关键词
70    ///
71    /// optional
72    /// 可选参数 : limit : 返回数量 , 默认为 30 offset : 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
73    /// type: 搜索类型;默认为 1 即单曲 , 取值意义 : 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合
74    pub async fn search(&self, key: &str, opt: Option<Value>) -> TResult<ApiResponse> {
75        self._search(key, "cloudsearch", opt).await
76    }
77
78    /// 说明 : 调用此接口,可收藏/取消收藏专辑
79    /// required
80    /// id : 专辑 id
81    /// t : 1 为收藏,其他为取消收藏
82    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    /// 说明 : 调用此接口 , 可获得已收藏专辑列表
95    /// optional
96    /// limit: 取出数量 , 默认为 25
97    /// offset: 偏移数量 , 用于分页 , 如 :( 页数 -1)*25, 其中 25 为 limit 的值 , 默认 为 0
98    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    /// 说明 : 调用此接口 , 传入专辑 id, 可获得专辑内容
109    /// required
110    /// 必选参数 : id: 专辑 id
111    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    /// 说明 : 调用此接口,可获取歌手全部歌曲 必选参数 :
119    /// required
120    /// id : 歌手 id
121    /// optional:
122    /// order : hot ,time 按照热门或者时间排序
123    /// limit: 取出歌单数量 , 默认为 50
124    /// offset: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)*50, 其中 50 为 limit 的值
125    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    /// 说明 : 调用此接口,可收藏歌手
143    /// required
144    /// id : 歌手 id
145    /// t:操作,1 为收藏,其他为取消收藏
146    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    /// 说明 : 调用此接口,可获取收藏的歌手列表
164    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    /// 说明 : 调用此接口,可获取歌手热门50首歌曲
175    /// required
176    /// id : 歌手 id
177    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    /// 说明: 调用此接口,传入歌曲 id, 可获取音乐是否可用,返回 { success: true, message: 'ok' } 或者 { success: false, message: '亲爱的,暂无版权' }
186    /// requried
187    /// 必选参数 : id : 歌曲 id
188    /// optional
189    /// 可选参数 : br: 码率,默认设置了 999000 即最大码率,如果要 320k 则可设置为 320000,其他类推
190    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    /// 说明 : 调用此接口 , 传入 type, 资源 id 可获得对应资源热门评论 ( 不需要登录 )
201    /// required
202    /// id : 资源 id
203    /// type: 数字 , 资源类型
204    ///
205    /// optional
206    /// 可选参数 : limit: 取出评论数量 , 默认为 20
207    /// offset: 偏移数量 , 用于分页 , 如 :( 评论页数 -1)*20, 其中 20 为 limit 的值
208    /// before: 分页参数,取上一页最后一项的 time 获取下一页数据(获取超过5000条评论的时候需要用到)
209    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    /// 新版评论接口
232    /// 说明 : 调用此接口 , 传入资源类型和资源id,以及排序方式,可获取对应资源的评论
233    ///
234    /// required
235    /// id : 资源 id, 如歌曲 id,mv id
236    /// type: 数字 , 资源类型 , 对应歌曲 , mv, 专辑 , 歌单 , 电台, 视频对应以下类型
237    ///
238    /// optional
239    /// pageNo:分页参数,第N页,默认为1
240    /// pageSize:分页参数,每页多少条数据,默认20
241    /// sortType: 排序方式,1:按推荐排序,2:按热度排序,3:按时间排序
242    /// cursor: 当sortType为3时且页数不是第一页时需传入,值为上一条数据的time
243    #[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    /// required
280    /// rid: resource id
281    /// rt:  resource type
282    /// cmt: comment body
283    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    /// required
301    /// rid: resource id
302    /// rt:  resource type
303    /// reid: the comment id of reply to
304    /// cmt: comment body
305    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    /// required
324    /// rid: resource id
325    /// rt:  resource type
326    /// cmtid: comment id
327    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    /// 说明 : 调用此接口 , 传入签到类型 ( 可不传 , 默认安卓端签到 ), 可签到 ( 需要登录 ), 其中安卓端签到可获得 3 点经验 , web/PC 端签到可获得 2 点经验
345    ///
346    /// optional
347    /// 可选参数 : type: 签到类型 , 默认 0, 其中 0 为安卓端签到 ,1 为 web/PC 签到
348    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    /// 说明 : 调用此接口 , 传入音乐 id, 可把该音乐从私人 FM 中移除至垃圾桶
358    ///
359    /// required
360    /// id: 歌曲 id
361    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    /// 说明 : 调用此接口 , 传入音乐 id, 可喜欢该音乐
376    ///
377    /// required
378    /// 必选参数 : id: 歌曲 id
379    ///
380    /// optional
381    /// 可选参数 : like: 布尔值 , 默认为 true 即喜欢 , 若传 false, 则取消喜欢
382    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    /// 说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐id列表(id数组)
395    ///
396    /// required
397    /// 必选参数 : uid: 用户 id
398    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    /// 必选参数 :
407    /// phone: 手机号码
408    /// password: 密码
409    ///
410    /// 可选参数 :
411    /// countrycode: 国家码,用于国外手机号登录,例如美国传入:1
412    /// md5_password: md5加密后的密码,传入后 password 将失效
413    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    /// 说明 : 调用此接口 , 可刷新登录状态
430    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    /// 说明 : 调用此接口,可获取登录状态
437    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    /// 说明 : 调用此接口 , 可退出登录
444    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    /// 说明 : 调用此接口 , 传入音乐 id 可获得对应音乐的歌词 ( 不需要登录 )
451    ///
452    /// required
453    /// 必选参数 : id: 音乐 id
454    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    /// 说明 : 私人 FM( 需要登录 )
469    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    /// 说明 : 歌单能看到歌单名字, 但看不到具体歌单内容 , 调用此接口 , 传入歌单 id,
476    /// 可以获取对应歌单内的所有的音乐(未登录状态只能获取不完整的歌单,登录后是完整的),
477    /// 但是返回的trackIds是完整的,tracks 则是不完整的,
478    /// 可拿全部 trackIds 请求一次 song/detail 接口获取所有歌曲的详情
479    ///
480    /// required
481    /// 必选参数 : id : 歌单 id
482    ///
483    /// optional
484    /// 可选参数 : s : 歌单最近的 s 个收藏者,默认为8
485    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    /// 说明 : 调用此接口 , 可以添加歌曲到歌单或者从歌单删除某首歌曲 ( 需要登录 )
495    ///
496    /// required
497    /// op: 从歌单增加单曲为 add, 删除为 del
498    /// pid: 歌单 id
499    /// tracks: 歌曲 id,可多个,用逗号隔开
500    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    /// 说明 : 登录后调用此接口,可以更新用户歌单
516    ///
517    /// required
518    /// id:歌单id
519    /// name:歌单名字
520    /// desc:歌单描述
521    /// tags:歌单tag ,多个用 `;` 隔开,只能用官方规定标签
522    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    /// 说明 : 调用此接口 , 可获得每日推荐歌单 ( 需要登录 )
542    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    /// 说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 )
549    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    /// 说明 : 调用此接口 , 传入音乐 id, 来源 id,歌曲时间 time,更新听歌排行数据
558    ///
559    /// requried
560    /// 必选参数 :
561    /// id: 歌曲 id
562    /// sourceid: 歌单或专辑 id
563    ///
564    /// optional
565    /// 可选参数 : time: 歌曲播放时间,单位为秒
566    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    /// 说明 : 调用此接口 , 可获取默认搜索关键词
589    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    /// 说明 : 调用此接口,可获取热门搜索列表
599    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    /// 说明 : 调用此接口,可获取热门搜索列表(简略)
606    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    /// 说明 : 调用此接口 , 传入搜索关键词可获得搜索建议 , 搜索结果同时包含单曲 , 歌手 , 歌单 ,mv 信息
616    ///
617    /// required
618    /// 必选参数 : keywords : 关键词
619    ///
620    /// optional
621    /// 可选参数 : type : 如果传 'mobile' 则返回移动端数据
622    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    /// 说明 : 调用此接口 , 传入歌手 id, 可获得相似歌手
639    ///
640    /// requried
641    /// 必选参数 : id: 歌手 id
642    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    /// 说明 : 调用此接口 , 传入歌曲 id, 可获得相似歌单
657    ///
658    /// required
659    /// 必选参数 : id: 歌曲 id
660    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    /// 说明 : 调用此接口 , 传入歌曲 id, 可获得相似歌曲
671    ///
672    /// required
673    /// 必选参数 : id: 歌曲 id
674    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    /// 说明 : 调用此接口 , 传入音乐 id(支持多个 id, 用 , 隔开), 可获得歌曲详情
685    ///
686    /// requried
687    /// 必选参数 : ids: 音乐 id, 如 ids=347230
688    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    /// 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ),
701    /// 可以获取对应的音乐的 url,未登录状态或者非会员返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)
702    ///
703    /// required
704    /// 必选参数 : id : 音乐 id
705    ///
706    /// optional
707    /// 可选参数 : br: 码率,默认设置了 999000 即最大码率,如果要 320k 则可设置为 320000,其他类推
708    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    /// 说明 : 登录后调用此接口 ,可获取用户账号信息
730    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    /// 说明 : 登录后调用此接口 , 传入云盘歌曲 id,可获取云盘数据详情
736    ///
737    /// requried
738    /// 必选参数 : id: 歌曲id,可多个,用逗号隔开
739    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    /// 说明 : 登录后调用此接口 , 可获取云盘数据 , 获取的数据没有对应 url, 需要再调用一 次 /song/url 获取 url
747    ///
748    /// optional
749    /// 可选参数 :
750    /// limit : 返回数量 , 默认为 200
751    /// offset : 偏移数量,用于分页 , 如 :( 页数 -1)*200, 其中 200 为 limit 的值 , 默认为 0
752    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    /// 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户历史评论
761    ///
762    /// requried
763    /// 必选参数 : uid : 用户 id
764    ///
765    /// optional
766    /// 可选参数 :
767    /// limit : 返回数量 , 默认为 10
768    /// time: 上一条数据的time,第一页不需要传,默认为0
769    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    /// 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户详情
788    ///
789    /// required
790    /// 必选参数 : uid : 用户 id
791    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    /// 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户电台
798    ///
799    /// required
800    /// 必选参数 : uid : 用户 id
801    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    /// 说明 : 调用此接口, 传入用户id可获取用户创建的电台
811    ///
812    /// required
813    /// 必选参数 : uid : 用户 id
814    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    /// 说明 : 登录后调用此接口 , 传入rid, 可查看对应电台的电台节目以及对应的 id, 需要 注意的是这个接口返回的 mp3Url 已经无效 , 都为 null, 但是通过调用 /song/url 这 个接口 , 传入节目 id 仍然能获取到节目音频 , 如 /song/url?id=478446370 获取代 码时间的一个节目的音频
822    /// 必选参数 : rid: 电台 的 id
823    /// 可选参数 :
824    /// limit : 返回数量 , 默认为 30
825    /// offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
826    /// asc : 排序方式,默认为 false (新 => 老 ) 设置 true 可改为 老 => 新
827    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    /// 说明 : 登录后调用此接口 , 可以获取用户等级信息,包含当前登录天数,听歌次数,下一等级需要的登录天数和听歌次数,当前等级进度
840    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    /// 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户歌单
846    ///
847    /// required
848    /// 必选参数 : uid : 用户 id
849    ///
850    /// optional
851    /// 可选参数 :
852    /// limit : 返回数量 , 默认为 30
853    /// offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0
854    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    /// 说明 : 登录后调用此接口 , 传入用户 id, 可获取用户播放记录
864    ///
865    /// requred
866    /// 必选参数 : uid : 用户 id
867    ///
868    /// optional
869    /// 可选参数 : type : type=1 时只返回 weekData, type=0 时返回 allData
870    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    /// 说明 : 登录后调用此接口 , 可以获取用户信息
879    /// 获取用户信息 , 歌单,收藏,mv, dj 数量
880    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/// 0: 歌曲 1: mv 2: 歌单 3: 专辑 4: 电台 5: 视频 6: 动态
899#[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/// 搜索类型;1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合
911#[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        // let api = NcmApi::default();
1366        // let resp = api.user_cloud_detail().await;
1367        // assert!(resp.is_ok());
1368
1369        // let res = resp.unwrap();
1370        // let res = res.deserialize_to_implict();
1371        // assert_eq!(res.code, 200);
1372    }
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}