Skip to main content

bpi_rs/video_ranking/
client.rs

1use crate::video_ranking::dynamic::{NewListRankData, RegionArchivesData};
2use crate::video_ranking::params::{
3    PopularSeriesOneParams, VideoPopularListParams, VideoRankingListParams,
4    VideoRegionDynamicParams, VideoRegionNewListParams, VideoRegionNewListRankParams,
5    VideoRegionTagDynamicParams,
6};
7use crate::video_ranking::popular::{PopularListData, PopularSeriesListData, PopularSeriesOneData};
8use crate::video_ranking::precious_videos::PreciousVideoData;
9use crate::video_ranking::ranking::RankingListData;
10use crate::video_ranking::{
11    POPULAR_LIST_ENDPOINT, POPULAR_PRECIOUS_ENDPOINT, POPULAR_SERIES_LIST_ENDPOINT,
12    POPULAR_SERIES_ONE_ENDPOINT, RANKING_LIST_ENDPOINT, REGION_DYNAMIC_ENDPOINT,
13    REGION_NEWLIST_ENDPOINT, REGION_NEWLIST_RANK_ENDPOINT, REGION_TAG_DYNAMIC_ENDPOINT,
14};
15use crate::{BilibiliRequest, BpiClient, BpiResult};
16
17/// Video ranking API client.
18#[derive(Clone, Copy)]
19pub struct VideoRankingClient<'a> {
20    pub(crate) client: &'a BpiClient,
21}
22
23impl<'a> VideoRankingClient<'a> {
24    pub(crate) fn new(client: &'a BpiClient) -> Self {
25        Self { client }
26    }
27
28    #[cfg(test)]
29    pub(crate) fn popular_list_endpoint(&self) -> &'static str {
30        POPULAR_LIST_ENDPOINT
31    }
32
33    #[cfg(test)]
34    pub(crate) fn popular_series_list_endpoint(&self) -> &'static str {
35        POPULAR_SERIES_LIST_ENDPOINT
36    }
37
38    #[cfg(test)]
39    pub(crate) fn popular_series_one_endpoint(&self) -> &'static str {
40        POPULAR_SERIES_ONE_ENDPOINT
41    }
42
43    #[cfg(test)]
44    pub(crate) fn popular_precious_endpoint(&self) -> &'static str {
45        POPULAR_PRECIOUS_ENDPOINT
46    }
47
48    #[cfg(test)]
49    pub(crate) fn ranking_list_endpoint(&self) -> &'static str {
50        RANKING_LIST_ENDPOINT
51    }
52
53    #[cfg(test)]
54    pub(crate) fn region_dynamic_endpoint(&self) -> &'static str {
55        REGION_DYNAMIC_ENDPOINT
56    }
57
58    #[cfg(test)]
59    pub(crate) fn region_tag_dynamic_endpoint(&self) -> &'static str {
60        REGION_TAG_DYNAMIC_ENDPOINT
61    }
62
63    #[cfg(test)]
64    pub(crate) fn region_newlist_endpoint(&self) -> &'static str {
65        REGION_NEWLIST_ENDPOINT
66    }
67
68    #[cfg(test)]
69    pub(crate) fn region_newlist_rank_endpoint(&self) -> &'static str {
70        REGION_NEWLIST_RANK_ENDPOINT
71    }
72
73    /// Gets the current popular video list.
74    pub async fn popular_list(&self, params: VideoPopularListParams) -> BpiResult<PopularListData> {
75        self.client
76            .get(POPULAR_LIST_ENDPOINT)
77            .query(&params.query_pairs())
78            .send_bpi_payload("video_ranking.popular_list")
79            .await
80    }
81
82    /// Gets all weekly popular series entries.
83    pub async fn popular_series_list(&self) -> BpiResult<PopularSeriesListData> {
84        self.client
85            .get(POPULAR_SERIES_LIST_ENDPOINT)
86            .send_bpi_payload("video_ranking.popular_series_list")
87            .await
88    }
89
90    /// Gets one weekly popular series detail.
91    pub async fn popular_series_one(
92        &self,
93        params: PopularSeriesOneParams,
94    ) -> BpiResult<PopularSeriesOneData> {
95        let signed_params = self.client.get_wbi_sign2(params.query_pairs()).await?;
96
97        self.client
98            .get(POPULAR_SERIES_ONE_ENDPOINT)
99            .query(&signed_params)
100            .send_bpi_payload("video_ranking.popular_series_one")
101            .await
102    }
103
104    /// Gets the curated must-watch popular videos.
105    pub async fn popular_precious(&self) -> BpiResult<PreciousVideoData> {
106        self.client
107            .get(POPULAR_PRECIOUS_ENDPOINT)
108            .send_bpi_payload("video_ranking.popular_precious")
109            .await
110    }
111
112    /// Gets a video ranking list.
113    pub async fn ranking_list(&self, params: VideoRankingListParams) -> BpiResult<RankingListData> {
114        self.client
115            .get(RANKING_LIST_ENDPOINT)
116            .query(&params.query_pairs())
117            .send_bpi_payload("video_ranking.ranking_list")
118            .await
119    }
120
121    /// Gets the latest video list for a region.
122    pub async fn region_dynamic(
123        &self,
124        params: VideoRegionDynamicParams,
125    ) -> BpiResult<RegionArchivesData> {
126        self.client
127            .get(REGION_DYNAMIC_ENDPOINT)
128            .query(&params.query_pairs())
129            .send_bpi_payload("video_ranking.region_dynamic")
130            .await
131    }
132
133    /// Gets recent interactive videos for a region tag.
134    pub async fn region_tag_dynamic(
135        &self,
136        params: VideoRegionTagDynamicParams,
137    ) -> BpiResult<RegionArchivesData> {
138        self.client
139            .get(REGION_TAG_DYNAMIC_ENDPOINT)
140            .query(&params.query_pairs())
141            .send_bpi_payload("video_ranking.region_tag_dynamic")
142            .await
143    }
144
145    /// Gets recent submissions for a region.
146    pub async fn region_newlist(
147        &self,
148        params: VideoRegionNewListParams,
149    ) -> BpiResult<RegionArchivesData> {
150        self.client
151            .get(REGION_NEWLIST_ENDPOINT)
152            .query(&params.query_pairs())
153            .send_bpi_payload("video_ranking.region_newlist")
154            .await
155    }
156
157    /// Gets ranked recent submissions for a region.
158    pub async fn region_newlist_rank(
159        &self,
160        params: VideoRegionNewListRankParams,
161    ) -> BpiResult<NewListRankData> {
162        self.client
163            .get(REGION_NEWLIST_RANK_ENDPOINT)
164            .query(&params.query_pairs())
165            .send_bpi_payload("video_ranking.region_newlist_rank")
166            .await
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use std::future::Future;
173
174    use crate::probe::contract::HttpMethod;
175    use crate::probe::endpoint_contract::EndpointContract;
176    use crate::video_ranking::dynamic::{NewListRankData, RegionArchivesData};
177    use crate::video_ranking::params::{
178        PopularSeriesOneParams, VideoNewListRankOrder, VideoPopularListParams,
179        VideoRankingListParams, VideoRankingType, VideoRegionDynamicParams,
180        VideoRegionNewListParams, VideoRegionNewListRankParams, VideoRegionTagDynamicParams,
181    };
182    use crate::video_ranking::popular::{
183        PopularListData, PopularSeriesListData, PopularSeriesOneData,
184    };
185    use crate::video_ranking::precious_videos::PreciousVideoData;
186    use crate::video_ranking::ranking::RankingListData;
187    use crate::{BpiClient, BpiResult};
188
189    fn assert_popular_list_future<F>(_future: F)
190    where
191        F: Future<Output = BpiResult<PopularListData>>,
192    {
193    }
194
195    fn assert_series_list_future<F>(_future: F)
196    where
197        F: Future<Output = BpiResult<PopularSeriesListData>>,
198    {
199    }
200
201    fn assert_series_one_future<F>(_future: F)
202    where
203        F: Future<Output = BpiResult<PopularSeriesOneData>>,
204    {
205    }
206
207    fn assert_precious_future<F>(_future: F)
208    where
209        F: Future<Output = BpiResult<PreciousVideoData>>,
210    {
211    }
212
213    fn assert_ranking_list_future<F>(_future: F)
214    where
215        F: Future<Output = BpiResult<RankingListData>>,
216    {
217    }
218
219    fn assert_region_archives_future<F>(_future: F)
220    where
221        F: Future<Output = BpiResult<RegionArchivesData>>,
222    {
223    }
224
225    fn assert_newlist_rank_future<F>(_future: F)
226    where
227        F: Future<Output = BpiResult<NewListRankData>>,
228    {
229    }
230
231    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
232        let bytes = match endpoint {
233            "popular-list" => include_bytes!(
234                "../../tests/contracts/video_ranking/read/popular-list/contract.json"
235            )
236            .as_slice(),
237            "popular-series-list" => include_bytes!(
238                "../../tests/contracts/video_ranking/read/popular-series-list/contract.json"
239            )
240            .as_slice(),
241            "popular-series-one" => include_bytes!(
242                "../../tests/contracts/video_ranking/read/popular-series-one/contract.json"
243            )
244            .as_slice(),
245            "popular-precious" => include_bytes!(
246                "../../tests/contracts/video_ranking/read/popular-precious/contract.json"
247            )
248            .as_slice(),
249            "ranking-list" => include_bytes!(
250                "../../tests/contracts/video_ranking/read/ranking-list/contract.json"
251            )
252            .as_slice(),
253            "region-dynamic" => include_bytes!(
254                "../../tests/contracts/video_ranking/read/region-dynamic/contract.json"
255            )
256            .as_slice(),
257            "region-tag-dynamic" => include_bytes!(
258                "../../tests/contracts/video_ranking/read/region-tag-dynamic/contract.json"
259            )
260            .as_slice(),
261            "region-newlist" => include_bytes!(
262                "../../tests/contracts/video_ranking/read/region-newlist/contract.json"
263            )
264            .as_slice(),
265            "region-newlist-rank" => include_bytes!(
266                "../../tests/contracts/video_ranking/read/region-newlist-rank/contract.json"
267            )
268            .as_slice(),
269            _ => unreachable!("unknown video_ranking contract"),
270        };
271        EndpointContract::from_slice(bytes)
272    }
273
274    #[test]
275    fn video_ranking_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
276        let client = BpiClient::new()?;
277        let video_ranking = client.video_ranking();
278
279        assert_eq!(
280            video_ranking.popular_list_endpoint(),
281            "https://api.bilibili.com/x/web-interface/popular"
282        );
283        assert_eq!(
284            video_ranking.popular_series_list_endpoint(),
285            "https://api.bilibili.com/x/web-interface/popular/series/list"
286        );
287        assert_eq!(
288            video_ranking.popular_series_one_endpoint(),
289            "https://api.bilibili.com/x/web-interface/popular/series/one"
290        );
291        assert_eq!(
292            video_ranking.popular_precious_endpoint(),
293            "https://api.bilibili.com/x/web-interface/popular/precious"
294        );
295        assert_eq!(
296            video_ranking.ranking_list_endpoint(),
297            "https://api.bilibili.com/x/web-interface/ranking/v2"
298        );
299        assert_eq!(
300            video_ranking.region_dynamic_endpoint(),
301            "https://api.bilibili.com/x/web-interface/dynamic/region"
302        );
303        assert_eq!(
304            video_ranking.region_tag_dynamic_endpoint(),
305            "https://api.bilibili.com/x/web-interface/dynamic/tag"
306        );
307        assert_eq!(
308            video_ranking.region_newlist_endpoint(),
309            "https://api.bilibili.com/x/web-interface/newlist"
310        );
311        assert_eq!(
312            video_ranking.region_newlist_rank_endpoint(),
313            "https://api.bilibili.com/x/web-interface/newlist_rank"
314        );
315        Ok(())
316    }
317
318    #[test]
319    fn video_ranking_methods_return_payload_futures() -> BpiResult<()> {
320        let client = BpiClient::new()?;
321        let video_ranking = client.video_ranking();
322
323        assert_popular_list_future(
324            video_ranking.popular_list(
325                VideoPopularListParams::new()
326                    .with_page(1)?
327                    .with_page_size(2)?,
328            ),
329        );
330        assert_series_list_future(video_ranking.popular_series_list());
331        assert_series_one_future(video_ranking.popular_series_one(PopularSeriesOneParams::new(1)?));
332        assert_precious_future(video_ranking.popular_precious());
333        assert_ranking_list_future(
334            video_ranking.ranking_list(
335                VideoRankingListParams::new()
336                    .with_rid(1)?
337                    .with_type(VideoRankingType::All),
338            ),
339        );
340        assert_region_archives_future(
341            video_ranking.region_dynamic(
342                VideoRegionDynamicParams::new(21)?
343                    .with_page(1)?
344                    .with_page_size(2)?,
345            ),
346        );
347        assert_region_archives_future(
348            video_ranking.region_tag_dynamic(
349                VideoRegionTagDynamicParams::new(136, 10026108)?
350                    .with_page(1)?
351                    .with_page_size(2)?,
352            ),
353        );
354        assert_region_archives_future(
355            video_ranking.region_newlist(
356                VideoRegionNewListParams::new(231)?
357                    .with_page(1)?
358                    .with_page_size(2)?
359                    .with_type(1)?,
360            ),
361        );
362        assert_newlist_rank_future(
363            video_ranking.region_newlist_rank(
364                VideoRegionNewListRankParams::new(231, 2, "20260701", "20260703")?
365                    .with_order(VideoNewListRankOrder::Click)
366                    .with_page(1)?,
367            ),
368        );
369        Ok(())
370    }
371
372    #[test]
373    fn video_ranking_contracts_match_module_client_endpoints() -> BpiResult<()> {
374        let client = BpiClient::new()?;
375        let video_ranking = client.video_ranking();
376
377        let expectations = [
378            (
379                "popular-list",
380                "video_ranking.popular_list",
381                video_ranking.popular_list_endpoint(),
382                false,
383            ),
384            (
385                "popular-series-list",
386                "video_ranking.popular_series_list",
387                video_ranking.popular_series_list_endpoint(),
388                false,
389            ),
390            (
391                "popular-series-one",
392                "video_ranking.popular_series_one",
393                video_ranking.popular_series_one_endpoint(),
394                true,
395            ),
396            (
397                "popular-precious",
398                "video_ranking.popular_precious",
399                video_ranking.popular_precious_endpoint(),
400                false,
401            ),
402            (
403                "ranking-list",
404                "video_ranking.ranking_list",
405                video_ranking.ranking_list_endpoint(),
406                false,
407            ),
408            (
409                "region-dynamic",
410                "video_ranking.region_dynamic",
411                video_ranking.region_dynamic_endpoint(),
412                false,
413            ),
414            (
415                "region-tag-dynamic",
416                "video_ranking.region_tag_dynamic",
417                video_ranking.region_tag_dynamic_endpoint(),
418                false,
419            ),
420            (
421                "region-newlist",
422                "video_ranking.region_newlist",
423                video_ranking.region_newlist_endpoint(),
424                false,
425            ),
426            (
427                "region-newlist-rank",
428                "video_ranking.region_newlist_rank",
429                video_ranking.region_newlist_rank_endpoint(),
430                false,
431            ),
432        ];
433
434        for (endpoint, name, url, requires_wbi) in expectations {
435            let contract = contract(endpoint)?;
436
437            assert_eq!(contract.name, name);
438            assert_eq!(contract.request.method, HttpMethod::Get);
439            assert_eq!(contract.request.url.as_str(), url);
440            assert_eq!(contract.request.auth.requires_wbi(), requires_wbi);
441        }
442
443        Ok(())
444    }
445}