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#[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 pub async fn popular_list(&self, params: VideoPopularListParams) -> BpiResult<PopularListData> {
75 self.client
76 .get(POPULAR_LIST_ENDPOINT)
77 .query(¶ms.query_pairs())
78 .send_bpi_payload("video_ranking.popular_list")
79 .await
80 }
81
82 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 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 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 pub async fn ranking_list(&self, params: VideoRankingListParams) -> BpiResult<RankingListData> {
114 self.client
115 .get(RANKING_LIST_ENDPOINT)
116 .query(¶ms.query_pairs())
117 .send_bpi_payload("video_ranking.ranking_list")
118 .await
119 }
120
121 pub async fn region_dynamic(
123 &self,
124 params: VideoRegionDynamicParams,
125 ) -> BpiResult<RegionArchivesData> {
126 self.client
127 .get(REGION_DYNAMIC_ENDPOINT)
128 .query(¶ms.query_pairs())
129 .send_bpi_payload("video_ranking.region_dynamic")
130 .await
131 }
132
133 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(¶ms.query_pairs())
141 .send_bpi_payload("video_ranking.region_tag_dynamic")
142 .await
143 }
144
145 pub async fn region_newlist(
147 &self,
148 params: VideoRegionNewListParams,
149 ) -> BpiResult<RegionArchivesData> {
150 self.client
151 .get(REGION_NEWLIST_ENDPOINT)
152 .query(¶ms.query_pairs())
153 .send_bpi_payload("video_ranking.region_newlist")
154 .await
155 }
156
157 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(¶ms.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}