Skip to main content

bpi_rs/creativecenter/
client.rs

1use crate::creativecenter::params::{
2    UpArchiveCompareParams, UpArchiveVideosParams, UpArchivesListParams, UpArticleTrendParams,
3    UpVideoTrendParams,
4};
5use crate::creativecenter::railgun::ElectromagneticInfo;
6use crate::creativecenter::season::list::SeasonListData;
7use crate::creativecenter::season::section::SeasonSectionEpisodesData;
8use crate::creativecenter::season::{
9    SeasonByAidData, SeasonByAidParams, SeasonInfoData, SeasonInfoParams, SeasonListParams,
10    SeasonSectionEpisodesParams,
11};
12use crate::creativecenter::statistics_data::{
13    ArchiveCompareData, ArticleTrendItem, PlaySourceData, UpArticleStatData, UpStatData,
14    VideoTrendItem, ViewerData,
15};
16use crate::creativecenter::videos::{ArchiveVideosData, SpArchivesData};
17use crate::{BilibiliRequest, BpiClient, BpiResult};
18
19const SEASON_LIST_ENDPOINT: &str = "https://member.bilibili.com/x2/creative/web/seasons";
20const SEASON_INFO_ENDPOINT: &str = "https://member.bilibili.com/x2/creative/web/season";
21const SEASON_BY_AID_ENDPOINT: &str = "https://member.bilibili.com/x2/creative/web/season/aid";
22const SEASON_SECTION_EPISODES_ENDPOINT: &str =
23    "https://member.bilibili.com/x2/creative/web/season/section";
24const ARCHIVES_LIST_ENDPOINT: &str = "https://member.bilibili.com/x2/creative/web/archives/sp";
25const ARCHIVE_VIDEOS_ENDPOINT: &str = "https://member.bilibili.com/x/web/archive/videos";
26const UP_STAT_ENDPOINT: &str = "https://member.bilibili.com/x/web/index/stat";
27const ARCHIVE_COMPARE_ENDPOINT: &str =
28    "https://member.bilibili.com/x/web/data/archive_diagnose/compare";
29const ARTICLE_STAT_ENDPOINT: &str = "https://member.bilibili.com/x/web/data/article";
30const VIDEO_TREND_ENDPOINT: &str = "https://member.bilibili.com/x/web/data/pandect";
31const ARTICLE_TREND_ENDPOINT: &str = "https://member.bilibili.com/x/web/data/article/thirty";
32const PLAY_SOURCE_ENDPOINT: &str = "https://member.bilibili.com/x/web/data/playsource";
33const VIEWER_DATA_ENDPOINT: &str = "https://member.bilibili.com/x/web/data/base";
34const ELECTROMAGNETIC_INFO_ENDPOINT: &str =
35    "https://api.bilibili.com/studio/up-rating/v3/rating/info";
36
37/// Creative center API client.
38#[derive(Clone, Copy)]
39pub struct CreativeCenterClient<'a> {
40    pub(crate) client: &'a BpiClient,
41}
42
43impl<'a> CreativeCenterClient<'a> {
44    pub(crate) fn new(client: &'a BpiClient) -> Self {
45        Self { client }
46    }
47
48    #[cfg(test)]
49    pub(crate) fn season_list_endpoint(&self) -> &'static str {
50        SEASON_LIST_ENDPOINT
51    }
52
53    #[cfg(test)]
54    pub(crate) fn season_info_endpoint(&self) -> &'static str {
55        SEASON_INFO_ENDPOINT
56    }
57
58    #[cfg(test)]
59    pub(crate) fn season_by_aid_endpoint(&self) -> &'static str {
60        SEASON_BY_AID_ENDPOINT
61    }
62
63    #[cfg(test)]
64    pub(crate) fn season_section_episodes_endpoint(&self) -> &'static str {
65        SEASON_SECTION_EPISODES_ENDPOINT
66    }
67
68    #[cfg(test)]
69    pub(crate) fn archives_list_endpoint(&self) -> &'static str {
70        ARCHIVES_LIST_ENDPOINT
71    }
72
73    #[cfg(test)]
74    pub(crate) fn archive_videos_endpoint(&self) -> &'static str {
75        ARCHIVE_VIDEOS_ENDPOINT
76    }
77
78    #[cfg(test)]
79    pub(crate) fn up_stat_endpoint(&self) -> &'static str {
80        UP_STAT_ENDPOINT
81    }
82
83    #[cfg(test)]
84    pub(crate) fn archive_compare_endpoint(&self) -> &'static str {
85        ARCHIVE_COMPARE_ENDPOINT
86    }
87
88    #[cfg(test)]
89    pub(crate) fn article_stat_endpoint(&self) -> &'static str {
90        ARTICLE_STAT_ENDPOINT
91    }
92
93    #[cfg(test)]
94    pub(crate) fn video_trend_endpoint(&self) -> &'static str {
95        VIDEO_TREND_ENDPOINT
96    }
97
98    #[cfg(test)]
99    pub(crate) fn article_trend_endpoint(&self) -> &'static str {
100        ARTICLE_TREND_ENDPOINT
101    }
102
103    #[cfg(test)]
104    pub(crate) fn play_source_endpoint(&self) -> &'static str {
105        PLAY_SOURCE_ENDPOINT
106    }
107
108    #[cfg(test)]
109    pub(crate) fn viewer_data_endpoint(&self) -> &'static str {
110        VIEWER_DATA_ENDPOINT
111    }
112
113    #[cfg(test)]
114    pub(crate) fn electromagnetic_info_endpoint(&self) -> &'static str {
115        ELECTROMAGNETIC_INFO_ENDPOINT
116    }
117
118    /// Lists seasons created by the current authenticated user.
119    pub async fn season_list(&self, params: SeasonListParams) -> BpiResult<SeasonListData> {
120        self.client
121            .get(SEASON_LIST_ENDPOINT)
122            .query(&params.query_pairs())
123            .send_bpi_payload("creativecenter.season.list")
124            .await
125    }
126
127    /// Gets one creative center season by season id.
128    pub async fn season_info(&self, params: SeasonInfoParams) -> BpiResult<SeasonInfoData> {
129        self.client
130            .get(SEASON_INFO_ENDPOINT)
131            .query(&params.query_pairs())
132            .send_bpi_payload("creativecenter.season.info")
133            .await
134    }
135
136    /// Gets the season that contains an archive id.
137    pub async fn season_by_aid(&self, params: SeasonByAidParams) -> BpiResult<SeasonByAidData> {
138        self.client
139            .get(SEASON_BY_AID_ENDPOINT)
140            .query(&params.query_pairs())
141            .send_bpi_payload("creativecenter.season.aid")
142            .await
143    }
144
145    /// Gets videos in a creative center season section.
146    pub async fn season_section_episodes(
147        &self,
148        params: SeasonSectionEpisodesParams,
149    ) -> BpiResult<SeasonSectionEpisodesData> {
150        self.client
151            .get(SEASON_SECTION_EPISODES_ENDPOINT)
152            .query(&params.query_pairs())
153            .send_bpi_payload("creativecenter.season.section")
154            .await
155    }
156
157    /// Lists archives for the current authenticated creator.
158    pub async fn archives_list(&self, params: UpArchivesListParams) -> BpiResult<SpArchivesData> {
159        self.client
160            .get(ARCHIVES_LIST_ENDPOINT)
161            .query(&params.query_pairs())
162            .send_bpi_payload("creativecenter.videos.archives_list")
163            .await
164    }
165
166    /// Gets basic video information for a creator archive.
167    pub async fn archive_videos(
168        &self,
169        params: UpArchiveVideosParams,
170    ) -> BpiResult<ArchiveVideosData> {
171        self.client
172            .get(ARCHIVE_VIDEOS_ENDPOINT)
173            .query(&params.query_pairs())
174            .send_bpi_payload("creativecenter.videos.archive_videos")
175            .await
176    }
177
178    /// Gets creator video statistics.
179    pub async fn up_stat(&self) -> BpiResult<UpStatData> {
180        self.client
181            .get(UP_STAT_ENDPOINT)
182            .send_bpi_payload("creativecenter.statistics.up_stat")
183            .await
184    }
185
186    /// Gets creator archive comparison statistics.
187    pub async fn archive_compare(
188        &self,
189        params: UpArchiveCompareParams,
190    ) -> BpiResult<ArchiveCompareData> {
191        self.client
192            .get(ARCHIVE_COMPARE_ENDPOINT)
193            .query(&params.query_pairs())
194            .send_bpi_payload("creativecenter.statistics.archive_compare")
195            .await
196    }
197
198    /// Gets creator article statistics.
199    pub async fn article_stat(&self) -> BpiResult<UpArticleStatData> {
200        self.client
201            .get(ARTICLE_STAT_ENDPOINT)
202            .send_bpi_payload("creativecenter.statistics.article_stat")
203            .await
204    }
205
206    /// Gets creator video trend data.
207    pub async fn video_trend(&self, params: UpVideoTrendParams) -> BpiResult<Vec<VideoTrendItem>> {
208        self.client
209            .get(VIDEO_TREND_ENDPOINT)
210            .query(&params.query_pairs())
211            .send_bpi_payload("creativecenter.statistics.video_trend")
212            .await
213    }
214
215    /// Gets creator article trend data.
216    pub async fn article_trend(
217        &self,
218        params: UpArticleTrendParams,
219    ) -> BpiResult<Vec<ArticleTrendItem>> {
220        self.client
221            .get(ARTICLE_TREND_ENDPOINT)
222            .query(&params.query_pairs())
223            .send_bpi_payload("creativecenter.statistics.article_trend")
224            .await
225    }
226
227    /// Gets creator playback source distribution.
228    pub async fn play_source(&self) -> BpiResult<PlaySourceData> {
229        self.client
230            .get(PLAY_SOURCE_ENDPOINT)
231            .with_bilibili_headers()
232            .send_bpi_payload("creativecenter.statistics.play_source")
233            .await
234    }
235
236    /// Gets creator viewer distribution data.
237    pub async fn viewer_data(&self) -> BpiResult<ViewerData> {
238        self.client
239            .get(VIEWER_DATA_ENDPOINT)
240            .send_bpi_payload("creativecenter.statistics.viewer_data")
241            .await
242    }
243
244    /// Gets current account electromagnetic rating information.
245    pub async fn electromagnetic_info(&self) -> BpiResult<ElectromagneticInfo> {
246        self.client
247            .get(ELECTROMAGNETIC_INFO_ENDPOINT)
248            .send_bpi_payload("creativecenter.railgun.electromagnetic_info")
249            .await
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::future::Future;
256
257    use crate::creativecenter::params::{
258        UpArchiveCompareParams, UpArchiveVideosParams, UpArchivesListParams, UpArticleTrendMetric,
259        UpArticleTrendParams, UpVideoTrendMetric, UpVideoTrendParams,
260    };
261    use crate::creativecenter::railgun::ElectromagneticInfo;
262    use crate::creativecenter::season::list::SeasonListData;
263    use crate::creativecenter::season::section::SeasonSectionEpisodesData;
264    use crate::creativecenter::season::{
265        SeasonByAidData, SeasonByAidParams, SeasonInfoData, SeasonInfoParams, SeasonListOrder,
266        SeasonListParams, SeasonListSort, SeasonSectionEpisodesParams,
267    };
268    use crate::creativecenter::statistics_data::{
269        ArchiveCompareData, ArticleTrendItem, PlaySourceData, UpArticleStatData, UpStatData,
270        VideoTrendItem, ViewerData,
271    };
272    use crate::creativecenter::videos::{ArchiveVideosData, SpArchivesData};
273    use crate::ids::{Aid, SeasonId};
274    use crate::probe::contract::HttpMethod;
275    use crate::probe::endpoint_contract::EndpointContract;
276    use crate::{BpiClient, BpiResult};
277
278    fn assert_season_list_future<F>(_future: F)
279    where
280        F: Future<Output = BpiResult<SeasonListData>>,
281    {
282    }
283
284    fn assert_season_info_future<F>(_future: F)
285    where
286        F: Future<Output = BpiResult<SeasonInfoData>>,
287    {
288    }
289
290    fn assert_season_by_aid_future<F>(_future: F)
291    where
292        F: Future<Output = BpiResult<SeasonByAidData>>,
293    {
294    }
295
296    fn assert_season_section_episodes_future<F>(_future: F)
297    where
298        F: Future<Output = BpiResult<SeasonSectionEpisodesData>>,
299    {
300    }
301
302    fn assert_archives_list_future<F>(_future: F)
303    where
304        F: Future<Output = BpiResult<SpArchivesData>>,
305    {
306    }
307
308    fn assert_archive_videos_future<F>(_future: F)
309    where
310        F: Future<Output = BpiResult<ArchiveVideosData>>,
311    {
312    }
313
314    fn assert_up_stat_future<F>(_future: F)
315    where
316        F: Future<Output = BpiResult<UpStatData>>,
317    {
318    }
319
320    fn assert_archive_compare_future<F>(_future: F)
321    where
322        F: Future<Output = BpiResult<ArchiveCompareData>>,
323    {
324    }
325
326    fn assert_article_stat_future<F>(_future: F)
327    where
328        F: Future<Output = BpiResult<UpArticleStatData>>,
329    {
330    }
331
332    fn assert_video_trend_future<F>(_future: F)
333    where
334        F: Future<Output = BpiResult<Vec<VideoTrendItem>>>,
335    {
336    }
337
338    fn assert_article_trend_future<F>(_future: F)
339    where
340        F: Future<Output = BpiResult<Vec<ArticleTrendItem>>>,
341    {
342    }
343
344    fn assert_play_source_future<F>(_future: F)
345    where
346        F: Future<Output = BpiResult<PlaySourceData>>,
347    {
348    }
349
350    fn assert_viewer_data_future<F>(_future: F)
351    where
352        F: Future<Output = BpiResult<ViewerData>>,
353    {
354    }
355
356    fn assert_electromagnetic_info_future<F>(_future: F)
357    where
358        F: Future<Output = BpiResult<ElectromagneticInfo>>,
359    {
360    }
361
362    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
363        let bytes = match endpoint {
364            "season-list" => include_bytes!(
365                "../../tests/contracts/creativecenter/season/list/contract.json"
366            )
367            .as_slice(),
368            "season-info" => include_bytes!(
369                "../../tests/contracts/creativecenter/season/info/contract.json"
370            )
371            .as_slice(),
372            "season-aid" => include_bytes!(
373                "../../tests/contracts/creativecenter/season/aid/contract.json"
374            )
375            .as_slice(),
376            "season-section" => include_bytes!(
377                "../../tests/contracts/creativecenter/season/section/contract.json"
378            )
379            .as_slice(),
380            "archives-list" => include_bytes!(
381                "../../tests/contracts/creativecenter/videos/archives-list/contract.json"
382            )
383            .as_slice(),
384            "archive-videos" => include_bytes!(
385                "../../tests/contracts/creativecenter/videos/archive-videos/contract.json"
386            )
387            .as_slice(),
388            "up-stat" => include_bytes!(
389                "../../tests/contracts/creativecenter/statistics/up-stat/contract.json"
390            )
391            .as_slice(),
392            "archive-compare" => include_bytes!(
393                "../../tests/contracts/creativecenter/statistics/archive-compare/contract.json"
394            )
395            .as_slice(),
396            "article-stat" => include_bytes!(
397                "../../tests/contracts/creativecenter/statistics/article-stat/contract.json"
398            )
399            .as_slice(),
400            "video-trend" => include_bytes!(
401                "../../tests/contracts/creativecenter/statistics/video-trend/contract.json"
402            )
403            .as_slice(),
404            "article-trend" => include_bytes!(
405                "../../tests/contracts/creativecenter/statistics/article-trend/contract.json"
406            )
407            .as_slice(),
408            "play-source" => include_bytes!(
409                "../../tests/contracts/creativecenter/statistics/play-source/contract.json"
410            )
411            .as_slice(),
412            "viewer-data" => include_bytes!(
413                "../../tests/contracts/creativecenter/statistics/viewer-data/contract.json"
414            )
415            .as_slice(),
416            "electromagnetic-info" => include_bytes!(
417                "../../tests/contracts/creativecenter/railgun-read/electromagnetic-info/contract.json"
418            )
419            .as_slice(),
420            _ => unreachable!("unknown creativecenter contract"),
421        };
422
423        EndpointContract::from_slice(bytes)
424    }
425
426    #[test]
427    fn creativecenter_methods_return_payload_futures() -> BpiResult<()> {
428        let client = BpiClient::new()?;
429        let creativecenter = client.creativecenter();
430
431        assert_season_list_future(
432            creativecenter.season_list(
433                SeasonListParams::new(1, 10)?
434                    .with_order(SeasonListOrder::CreatedAt)
435                    .with_sort(SeasonListSort::Desc),
436            ),
437        );
438        assert_season_info_future(
439            creativecenter.season_info(SeasonInfoParams::new(SeasonId::new(4294056)?)),
440        );
441        assert_season_by_aid_future(
442            creativecenter.season_by_aid(SeasonByAidParams::new(Aid::new(113602455409683)?)),
443        );
444        assert_season_section_episodes_future(
445            creativecenter
446                .season_section_episodes(SeasonSectionEpisodesParams::new(SeasonId::new(176088)?)),
447        );
448        assert_archives_list_future(
449            creativecenter.archives_list(UpArchivesListParams::new(1)?.with_page_size(10)?),
450        );
451        assert_archive_videos_future(
452            creativecenter.archive_videos(UpArchiveVideosParams::new(Aid::new(113602455409683)?)),
453        );
454        assert_up_stat_future(creativecenter.up_stat());
455        assert_archive_compare_future(
456            creativecenter.archive_compare(
457                UpArchiveCompareParams::new()
458                    .with_timestamp(1_720_000_000)?
459                    .with_size(1)?,
460            ),
461        );
462        assert_article_stat_future(creativecenter.article_stat());
463        assert_video_trend_future(
464            creativecenter.video_trend(UpVideoTrendParams::new(UpVideoTrendMetric::Play)),
465        );
466        assert_article_trend_future(
467            creativecenter.article_trend(UpArticleTrendParams::new(UpArticleTrendMetric::Read)),
468        );
469        assert_play_source_future(creativecenter.play_source());
470        assert_viewer_data_future(creativecenter.viewer_data());
471        assert_electromagnetic_info_future(creativecenter.electromagnetic_info());
472        Ok(())
473    }
474
475    #[test]
476    fn creativecenter_contracts_match_module_client_endpoints() -> BpiResult<()> {
477        let client = BpiClient::new()?;
478        let creativecenter = client.creativecenter();
479
480        let expectations = [
481            (
482                "season-list",
483                "creativecenter.season.list",
484                creativecenter.season_list_endpoint(),
485            ),
486            (
487                "season-info",
488                "creativecenter.season.info",
489                creativecenter.season_info_endpoint(),
490            ),
491            (
492                "season-aid",
493                "creativecenter.season.aid",
494                creativecenter.season_by_aid_endpoint(),
495            ),
496            (
497                "season-section",
498                "creativecenter.season.section",
499                creativecenter.season_section_episodes_endpoint(),
500            ),
501            (
502                "archives-list",
503                "creativecenter.videos.archives_list",
504                creativecenter.archives_list_endpoint(),
505            ),
506            (
507                "archive-videos",
508                "creativecenter.videos.archive_videos",
509                creativecenter.archive_videos_endpoint(),
510            ),
511            (
512                "up-stat",
513                "creativecenter.statistics.up_stat",
514                creativecenter.up_stat_endpoint(),
515            ),
516            (
517                "archive-compare",
518                "creativecenter.statistics.archive_compare",
519                creativecenter.archive_compare_endpoint(),
520            ),
521            (
522                "article-stat",
523                "creativecenter.statistics.article_stat",
524                creativecenter.article_stat_endpoint(),
525            ),
526            (
527                "video-trend",
528                "creativecenter.statistics.video_trend",
529                creativecenter.video_trend_endpoint(),
530            ),
531            (
532                "article-trend",
533                "creativecenter.statistics.article_trend",
534                creativecenter.article_trend_endpoint(),
535            ),
536            (
537                "play-source",
538                "creativecenter.statistics.play_source",
539                creativecenter.play_source_endpoint(),
540            ),
541            (
542                "viewer-data",
543                "creativecenter.statistics.viewer_data",
544                creativecenter.viewer_data_endpoint(),
545            ),
546            (
547                "electromagnetic-info",
548                "creativecenter.railgun.electromagnetic_info",
549                creativecenter.electromagnetic_info_endpoint(),
550            ),
551        ];
552
553        for (endpoint, name, url) in expectations {
554            let contract = contract(endpoint)?;
555
556            assert_eq!(contract.name, name);
557            assert_eq!(contract.request.method, HttpMethod::Get);
558            assert_eq!(contract.request.url.as_str(), url);
559        }
560
561        Ok(())
562    }
563}