1use crate::audio::info::{AudioInfoData, AudioMemberType, AudioTag};
2use crate::audio::music_list::{
3 AudioCollection, AudioCollectionsListData, AudioHotMenuData, AudioRankMenuData,
4};
5use crate::audio::musicstream_url::{AudioStreamUrlData, AudioStreamUrlWebData};
6use crate::audio::params::{
7 AudioCollectionInfoParams, AudioPageParams, AudioRankListParams, AudioRankPeriodParams,
8 AudioSongParams, AudioStreamUrlParams, AudioStreamUrlWebParams,
9};
10use crate::audio::rank::{AudioRankDetailData, AudioRankMusicListData, AudioRankPeriodData};
11use crate::audio::status_number::AudioStatusNumberData;
12use crate::{BilibiliRequest, BpiClient, BpiResult};
13
14const INFO_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/song/info";
15const TAGS_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/tag/song";
16const MEMBERS_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/member/song";
17const LYRIC_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/song/lyric";
18const STATUS_NUMBER_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/stat/song";
19const COLLECTION_STATUS_ENDPOINT: &str =
20 "https://www.bilibili.com/audio/music-service-c/web/collections/songs-coll";
21const COIN_COUNT_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/coin/audio";
22const STREAM_URL_WEB_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/url";
23const STREAM_URL_ENDPOINT: &str = "https://api.bilibili.com/audio/music-service-c/url";
24const COLLECTIONS_LIST_ENDPOINT: &str =
25 "https://www.bilibili.com/audio/music-service-c/web/collections/list";
26const COLLECTION_INFO_ENDPOINT: &str =
27 "https://www.bilibili.com/audio/music-service-c/web/collections/info";
28const HOT_MENU_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/menu/hit";
29const RANK_MENU_ENDPOINT: &str = "https://www.bilibili.com/audio/music-service-c/web/menu/rank";
30const RANK_PERIOD_ENDPOINT: &str =
31 "https://api.bilibili.com/x/copyright-music-publicity/toplist/all_period";
32const RANK_DETAIL_ENDPOINT: &str =
33 "https://api.bilibili.com/x/copyright-music-publicity/toplist/detail";
34const RANK_MUSIC_LIST_ENDPOINT: &str =
35 "https://api.bilibili.com/x/copyright-music-publicity/toplist/music_list";
36
37#[derive(Clone, Copy)]
39pub struct AudioClient<'a> {
40 pub(crate) client: &'a BpiClient,
41}
42
43impl<'a> AudioClient<'a> {
44 pub(crate) fn new(client: &'a BpiClient) -> Self {
45 Self { client }
46 }
47
48 #[cfg(test)]
49 pub(crate) fn info_endpoint(&self) -> &'static str {
50 INFO_ENDPOINT
51 }
52
53 #[cfg(test)]
54 pub(crate) fn tags_endpoint(&self) -> &'static str {
55 TAGS_ENDPOINT
56 }
57
58 #[cfg(test)]
59 pub(crate) fn members_endpoint(&self) -> &'static str {
60 MEMBERS_ENDPOINT
61 }
62
63 #[cfg(test)]
64 pub(crate) fn lyric_endpoint(&self) -> &'static str {
65 LYRIC_ENDPOINT
66 }
67
68 #[cfg(test)]
69 pub(crate) fn status_number_endpoint(&self) -> &'static str {
70 STATUS_NUMBER_ENDPOINT
71 }
72
73 #[cfg(test)]
74 pub(crate) fn collection_status_endpoint(&self) -> &'static str {
75 COLLECTION_STATUS_ENDPOINT
76 }
77
78 #[cfg(test)]
79 pub(crate) fn coin_count_endpoint(&self) -> &'static str {
80 COIN_COUNT_ENDPOINT
81 }
82
83 #[cfg(test)]
84 pub(crate) fn stream_url_web_endpoint(&self) -> &'static str {
85 STREAM_URL_WEB_ENDPOINT
86 }
87
88 #[cfg(test)]
89 pub(crate) fn stream_url_endpoint(&self) -> &'static str {
90 STREAM_URL_ENDPOINT
91 }
92
93 #[cfg(test)]
94 pub(crate) fn collections_list_endpoint(&self) -> &'static str {
95 COLLECTIONS_LIST_ENDPOINT
96 }
97
98 #[cfg(test)]
99 pub(crate) fn collection_info_endpoint(&self) -> &'static str {
100 COLLECTION_INFO_ENDPOINT
101 }
102
103 #[cfg(test)]
104 pub(crate) fn hot_menu_endpoint(&self) -> &'static str {
105 HOT_MENU_ENDPOINT
106 }
107
108 #[cfg(test)]
109 pub(crate) fn rank_menu_endpoint(&self) -> &'static str {
110 RANK_MENU_ENDPOINT
111 }
112
113 #[cfg(test)]
114 pub(crate) fn rank_period_endpoint(&self) -> &'static str {
115 RANK_PERIOD_ENDPOINT
116 }
117
118 #[cfg(test)]
119 pub(crate) fn rank_detail_endpoint(&self) -> &'static str {
120 RANK_DETAIL_ENDPOINT
121 }
122
123 #[cfg(test)]
124 pub(crate) fn rank_music_list_endpoint(&self) -> &'static str {
125 RANK_MUSIC_LIST_ENDPOINT
126 }
127
128 pub async fn info(&self, params: AudioSongParams) -> BpiResult<AudioInfoData> {
130 self.client
131 .get(INFO_ENDPOINT)
132 .query(¶ms.query_pairs())
133 .send_bpi_payload("audio.info")
134 .await
135 }
136
137 pub async fn tags(&self, params: AudioSongParams) -> BpiResult<Vec<AudioTag>> {
139 self.client
140 .get(TAGS_ENDPOINT)
141 .query(¶ms.query_pairs())
142 .send_bpi_payload("audio.tags")
143 .await
144 }
145
146 pub async fn members(&self, params: AudioSongParams) -> BpiResult<Vec<AudioMemberType>> {
148 self.client
149 .get(MEMBERS_ENDPOINT)
150 .query(¶ms.query_pairs())
151 .send_bpi_payload("audio.members")
152 .await
153 }
154
155 pub async fn lyric(&self, params: AudioSongParams) -> BpiResult<String> {
157 self.client
158 .get(LYRIC_ENDPOINT)
159 .query(¶ms.query_pairs())
160 .send_bpi_payload("audio.lyric")
161 .await
162 }
163
164 pub async fn status_number(&self, params: AudioSongParams) -> BpiResult<AudioStatusNumberData> {
166 self.client
167 .get(STATUS_NUMBER_ENDPOINT)
168 .query(¶ms.query_pairs())
169 .send_bpi_payload("audio.status_number")
170 .await
171 }
172
173 pub async fn collection_status(&self, params: AudioSongParams) -> BpiResult<bool> {
175 self.client
176 .get(COLLECTION_STATUS_ENDPOINT)
177 .query(¶ms.query_pairs())
178 .send_bpi_payload("audio.collection_status")
179 .await
180 }
181
182 pub async fn coin_count(&self, params: AudioSongParams) -> BpiResult<i32> {
184 self.client
185 .get(COIN_COUNT_ENDPOINT)
186 .query(¶ms.query_pairs())
187 .send_bpi_payload("audio.coin_count")
188 .await
189 }
190
191 pub async fn stream_url_web(
193 &self,
194 params: AudioStreamUrlWebParams,
195 ) -> BpiResult<AudioStreamUrlWebData> {
196 self.client
197 .get(STREAM_URL_WEB_ENDPOINT)
198 .query(¶ms.query_pairs())
199 .send_bpi_payload("audio.stream_url_web")
200 .await
201 }
202
203 pub async fn stream_url(&self, params: AudioStreamUrlParams) -> BpiResult<AudioStreamUrlData> {
205 self.client
206 .get(STREAM_URL_ENDPOINT)
207 .with_bilibili_headers()
208 .query(¶ms.query_pairs())
209 .send_bpi_payload("audio.stream_url")
210 .await
211 }
212
213 pub async fn collections_list(
215 &self,
216 params: AudioPageParams,
217 ) -> BpiResult<AudioCollectionsListData> {
218 self.client
219 .get(COLLECTIONS_LIST_ENDPOINT)
220 .query(¶ms.query_pairs())
221 .send_bpi_payload("audio.collections_list")
222 .await
223 }
224
225 pub async fn collection_info(
227 &self,
228 params: AudioCollectionInfoParams,
229 ) -> BpiResult<Option<AudioCollection>> {
230 self.client
231 .get(COLLECTION_INFO_ENDPOINT)
232 .query(¶ms.query_pairs())
233 .send_bpi_optional_payload("audio.collection_info")
234 .await
235 }
236
237 pub async fn hot_menu(&self, params: AudioPageParams) -> BpiResult<AudioHotMenuData> {
239 self.client
240 .get(HOT_MENU_ENDPOINT)
241 .query(¶ms.query_pairs())
242 .send_bpi_payload("audio.hot_menu")
243 .await
244 }
245
246 pub async fn rank_menu(&self, params: AudioPageParams) -> BpiResult<AudioRankMenuData> {
248 self.client
249 .get(RANK_MENU_ENDPOINT)
250 .query(¶ms.query_pairs())
251 .send_bpi_payload("audio.rank_menu")
252 .await
253 }
254
255 pub async fn rank_period(
257 &self,
258 params: AudioRankPeriodParams,
259 ) -> BpiResult<AudioRankPeriodData> {
260 let csrf = self.client.csrf().unwrap_or_default();
261
262 self.client
263 .get(RANK_PERIOD_ENDPOINT)
264 .query(¶ms.query_pairs(&csrf))
265 .send_bpi_payload("audio.rank_period")
266 .await
267 }
268
269 pub async fn rank_detail(&self, params: AudioRankListParams) -> BpiResult<AudioRankDetailData> {
271 let csrf = self.client.csrf().unwrap_or_default();
272
273 self.client
274 .get(RANK_DETAIL_ENDPOINT)
275 .query(¶ms.query_pairs(&csrf))
276 .send_bpi_payload("audio.rank_detail")
277 .await
278 }
279
280 pub async fn rank_music_list(
282 &self,
283 params: AudioRankListParams,
284 ) -> BpiResult<AudioRankMusicListData> {
285 let csrf = self.client.csrf().unwrap_or_default();
286
287 self.client
288 .get(RANK_MUSIC_LIST_ENDPOINT)
289 .query(¶ms.query_pairs(&csrf))
290 .send_bpi_payload("audio.rank_music_list")
291 .await
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use std::future::Future;
298
299 use crate::audio::info::{AudioInfoData, AudioMemberType, AudioTag};
300 use crate::audio::music_list::{
301 AudioCollection, AudioCollectionsListData, AudioHotMenuData, AudioRankMenuData,
302 };
303 use crate::audio::musicstream_url::{AudioQuality, AudioStreamUrlData, AudioStreamUrlWebData};
304 use crate::audio::params::{
305 AudioCollectionInfoParams, AudioPageParams, AudioRankListParams, AudioRankListType,
306 AudioRankPeriodParams, AudioSongParams, AudioStreamUrlParams, AudioStreamUrlWebParams,
307 };
308 use crate::audio::rank::{AudioRankDetailData, AudioRankMusicListData, AudioRankPeriodData};
309 use crate::audio::status_number::AudioStatusNumberData;
310 use crate::ids::AudioId;
311 use crate::probe::contract::HttpMethod;
312 use crate::probe::endpoint_contract::EndpointContract;
313 use crate::{BpiClient, BpiResult};
314
315 fn assert_info_future<F>(_future: F)
316 where
317 F: Future<Output = BpiResult<AudioInfoData>>,
318 {
319 }
320
321 fn assert_tags_future<F>(_future: F)
322 where
323 F: Future<Output = BpiResult<Vec<AudioTag>>>,
324 {
325 }
326
327 fn assert_members_future<F>(_future: F)
328 where
329 F: Future<Output = BpiResult<Vec<AudioMemberType>>>,
330 {
331 }
332
333 fn assert_lyric_future<F>(_future: F)
334 where
335 F: Future<Output = BpiResult<String>>,
336 {
337 }
338
339 fn assert_status_number_future<F>(_future: F)
340 where
341 F: Future<Output = BpiResult<AudioStatusNumberData>>,
342 {
343 }
344
345 fn assert_bool_future<F>(_future: F)
346 where
347 F: Future<Output = BpiResult<bool>>,
348 {
349 }
350
351 fn assert_coin_count_future<F>(_future: F)
352 where
353 F: Future<Output = BpiResult<i32>>,
354 {
355 }
356
357 fn assert_stream_url_web_future<F>(_future: F)
358 where
359 F: Future<Output = BpiResult<AudioStreamUrlWebData>>,
360 {
361 }
362
363 fn assert_stream_url_future<F>(_future: F)
364 where
365 F: Future<Output = BpiResult<AudioStreamUrlData>>,
366 {
367 }
368
369 fn assert_collections_list_future<F>(_future: F)
370 where
371 F: Future<Output = BpiResult<AudioCollectionsListData>>,
372 {
373 }
374
375 fn assert_collection_info_future<F>(_future: F)
376 where
377 F: Future<Output = BpiResult<Option<AudioCollection>>>,
378 {
379 }
380
381 fn assert_hot_menu_future<F>(_future: F)
382 where
383 F: Future<Output = BpiResult<AudioHotMenuData>>,
384 {
385 }
386
387 fn assert_rank_menu_future<F>(_future: F)
388 where
389 F: Future<Output = BpiResult<AudioRankMenuData>>,
390 {
391 }
392
393 fn assert_rank_period_future<F>(_future: F)
394 where
395 F: Future<Output = BpiResult<AudioRankPeriodData>>,
396 {
397 }
398
399 fn assert_rank_detail_future<F>(_future: F)
400 where
401 F: Future<Output = BpiResult<AudioRankDetailData>>,
402 {
403 }
404
405 fn assert_rank_music_list_future<F>(_future: F)
406 where
407 F: Future<Output = BpiResult<AudioRankMusicListData>>,
408 {
409 }
410
411 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
412 let bytes = match endpoint {
413 "info" => include_bytes!("../../tests/contracts/audio/info/contract.json").as_slice(),
414 "tags" => include_bytes!("../../tests/contracts/audio/tags/contract.json").as_slice(),
415 "members" => {
416 include_bytes!("../../tests/contracts/audio/members/contract.json").as_slice()
417 }
418 "lyric" => include_bytes!("../../tests/contracts/audio/lyric/contract.json").as_slice(),
419 "status-number" => {
420 include_bytes!("../../tests/contracts/audio/status-number/contract.json").as_slice()
421 }
422 "collection-status" => {
423 include_bytes!("../../tests/contracts/audio/collection-status/contract.json")
424 .as_slice()
425 }
426 "coin-count" => {
427 include_bytes!("../../tests/contracts/audio/coin-count/contract.json").as_slice()
428 }
429 "stream-url-web" => {
430 include_bytes!("../../tests/contracts/audio/stream-url-web/contract.json")
431 .as_slice()
432 }
433 "stream-url" => {
434 include_bytes!("../../tests/contracts/audio/stream-url/contract.json").as_slice()
435 }
436 "collections-list" => {
437 include_bytes!("../../tests/contracts/audio/collections-list/contract.json")
438 .as_slice()
439 }
440 "collection-info" => {
441 include_bytes!("../../tests/contracts/audio/collection-info/contract.json")
442 .as_slice()
443 }
444 "hot-menu" => {
445 include_bytes!("../../tests/contracts/audio/hot-menu/contract.json").as_slice()
446 }
447 "rank-menu" => {
448 include_bytes!("../../tests/contracts/audio/rank-menu/contract.json").as_slice()
449 }
450 "rank-period" => {
451 include_bytes!("../../tests/contracts/audio/rank-period/contract.json").as_slice()
452 }
453 "rank-detail" => {
454 include_bytes!("../../tests/contracts/audio/rank-detail/contract.json").as_slice()
455 }
456 "rank-music-list" => {
457 include_bytes!("../../tests/contracts/audio/rank-music-list/contract.json")
458 .as_slice()
459 }
460 _ => unreachable!("unknown audio contract"),
461 };
462
463 EndpointContract::from_slice(bytes)
464 }
465
466 #[test]
467 fn audio_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
468 let client = BpiClient::new()?;
469 let audio = client.audio();
470
471 assert_eq!(
472 audio.info_endpoint(),
473 "https://www.bilibili.com/audio/music-service-c/web/song/info"
474 );
475 assert_eq!(
476 audio.tags_endpoint(),
477 "https://www.bilibili.com/audio/music-service-c/web/tag/song"
478 );
479 assert_eq!(
480 audio.members_endpoint(),
481 "https://www.bilibili.com/audio/music-service-c/web/member/song"
482 );
483 assert_eq!(
484 audio.lyric_endpoint(),
485 "https://www.bilibili.com/audio/music-service-c/web/song/lyric"
486 );
487 assert_eq!(
488 audio.status_number_endpoint(),
489 "https://www.bilibili.com/audio/music-service-c/web/stat/song"
490 );
491 assert_eq!(
492 audio.collection_status_endpoint(),
493 "https://www.bilibili.com/audio/music-service-c/web/collections/songs-coll"
494 );
495 assert_eq!(
496 audio.coin_count_endpoint(),
497 "https://www.bilibili.com/audio/music-service-c/web/coin/audio"
498 );
499 assert_eq!(
500 audio.stream_url_web_endpoint(),
501 "https://www.bilibili.com/audio/music-service-c/web/url"
502 );
503 assert_eq!(
504 audio.stream_url_endpoint(),
505 "https://api.bilibili.com/audio/music-service-c/url"
506 );
507 assert_eq!(
508 audio.collections_list_endpoint(),
509 "https://www.bilibili.com/audio/music-service-c/web/collections/list"
510 );
511 assert_eq!(
512 audio.collection_info_endpoint(),
513 "https://www.bilibili.com/audio/music-service-c/web/collections/info"
514 );
515 assert_eq!(
516 audio.hot_menu_endpoint(),
517 "https://www.bilibili.com/audio/music-service-c/web/menu/hit"
518 );
519 assert_eq!(
520 audio.rank_menu_endpoint(),
521 "https://www.bilibili.com/audio/music-service-c/web/menu/rank"
522 );
523 assert_eq!(
524 audio.rank_period_endpoint(),
525 "https://api.bilibili.com/x/copyright-music-publicity/toplist/all_period"
526 );
527 assert_eq!(
528 audio.rank_detail_endpoint(),
529 "https://api.bilibili.com/x/copyright-music-publicity/toplist/detail"
530 );
531 assert_eq!(
532 audio.rank_music_list_endpoint(),
533 "https://api.bilibili.com/x/copyright-music-publicity/toplist/music_list"
534 );
535 Ok(())
536 }
537
538 #[test]
539 fn audio_methods_return_payload_futures() -> BpiResult<()> {
540 let client = BpiClient::new()?;
541 let audio = client.audio();
542 let sid = AudioId::new(13603)?;
543 let stream_sid = AudioId::new(15664)?;
544
545 assert_info_future(audio.info(AudioSongParams::new(sid)));
546 assert_tags_future(audio.tags(AudioSongParams::new(sid)));
547 assert_members_future(audio.members(AudioSongParams::new(sid)));
548 assert_lyric_future(audio.lyric(AudioSongParams::new(sid)));
549 assert_status_number_future(audio.status_number(AudioSongParams::new(sid)));
550 assert_bool_future(audio.collection_status(AudioSongParams::new(sid)));
551 assert_coin_count_future(audio.coin_count(AudioSongParams::new(sid)));
552 assert_stream_url_web_future(audio.stream_url_web(AudioStreamUrlWebParams::new(sid)));
553 assert_stream_url_future(audio.stream_url(AudioStreamUrlParams::new(
554 stream_sid,
555 AudioQuality::HighQuality,
556 )));
557 assert_collections_list_future(audio.collections_list(AudioPageParams::new(1, 2)?));
558 assert_collection_info_future(
559 audio.collection_info(AudioCollectionInfoParams::new(15_967_839)?),
560 );
561 assert_hot_menu_future(audio.hot_menu(AudioPageParams::new(1, 3)?));
562 assert_rank_menu_future(audio.rank_menu(AudioPageParams::new(1, 6)?));
563 assert_rank_period_future(
564 audio.rank_period(AudioRankPeriodParams::new(AudioRankListType::Original)),
565 );
566 assert_rank_detail_future(audio.rank_detail(AudioRankListParams::new(76)?));
567 assert_rank_music_list_future(audio.rank_music_list(AudioRankListParams::new(76)?));
568 Ok(())
569 }
570
571 #[test]
572 fn audio_contracts_match_module_client_endpoints() -> BpiResult<()> {
573 let client = BpiClient::new()?;
574 let audio = client.audio();
575
576 let expectations = [
577 ("info", "audio.info", audio.info_endpoint()),
578 ("tags", "audio.tags", audio.tags_endpoint()),
579 ("members", "audio.members", audio.members_endpoint()),
580 ("lyric", "audio.lyric", audio.lyric_endpoint()),
581 (
582 "status-number",
583 "audio.status_number",
584 audio.status_number_endpoint(),
585 ),
586 (
587 "collection-status",
588 "audio.collection_status",
589 audio.collection_status_endpoint(),
590 ),
591 (
592 "coin-count",
593 "audio.coin_count",
594 audio.coin_count_endpoint(),
595 ),
596 (
597 "stream-url-web",
598 "audio.stream_url_web",
599 audio.stream_url_web_endpoint(),
600 ),
601 (
602 "stream-url",
603 "audio.stream_url",
604 audio.stream_url_endpoint(),
605 ),
606 (
607 "collections-list",
608 "audio.collections_list",
609 audio.collections_list_endpoint(),
610 ),
611 (
612 "collection-info",
613 "audio.collection_info",
614 audio.collection_info_endpoint(),
615 ),
616 ("hot-menu", "audio.hot_menu", audio.hot_menu_endpoint()),
617 ("rank-menu", "audio.rank_menu", audio.rank_menu_endpoint()),
618 (
619 "rank-period",
620 "audio.rank_period",
621 audio.rank_period_endpoint(),
622 ),
623 (
624 "rank-detail",
625 "audio.rank_detail",
626 audio.rank_detail_endpoint(),
627 ),
628 (
629 "rank-music-list",
630 "audio.rank_music_list",
631 audio.rank_music_list_endpoint(),
632 ),
633 ];
634
635 for (endpoint, name, url) in expectations {
636 let contract = contract(endpoint)?;
637
638 assert_eq!(contract.name, name);
639 assert_eq!(contract.request.method, HttpMethod::Get);
640 assert_eq!(contract.request.url.as_str(), url);
641 }
642 Ok(())
643 }
644}