Skip to main content

bpi_rs/cheese/
client.rs

1use crate::cheese::info::{CourseEpList, CourseInfo};
2use crate::cheese::videostream_url::CourseVideoStreamData;
3use crate::cheese::{CheeseEpListParams, CheeseInfoParams, CheeseVideoStreamParams};
4use crate::ids::{EpisodeId, SeasonId};
5use crate::{BilibiliRequest, BpiClient, BpiResult};
6
7const INFO_ENDPOINT: &str = "https://api.bilibili.com/pugv/view/web/season";
8const EP_LIST_ENDPOINT: &str = "https://api.bilibili.com/pugv/view/web/ep/list";
9const VIDEO_STREAM_ENDPOINT: &str = "https://api.bilibili.com/pugv/player/web/playurl";
10
11/// Cheese course API client.
12#[derive(Clone, Copy)]
13pub struct CheeseClient<'a> {
14    pub(crate) client: &'a BpiClient,
15}
16
17impl<'a> CheeseClient<'a> {
18    pub(crate) fn new(client: &'a BpiClient) -> Self {
19        Self { client }
20    }
21
22    #[cfg(test)]
23    pub(crate) fn info_endpoint(&self) -> &'static str {
24        INFO_ENDPOINT
25    }
26
27    #[cfg(test)]
28    pub(crate) fn ep_list_endpoint(&self) -> &'static str {
29        EP_LIST_ENDPOINT
30    }
31
32    #[cfg(test)]
33    pub(crate) fn video_stream_endpoint(&self) -> &'static str {
34        VIDEO_STREAM_ENDPOINT
35    }
36
37    /// Gets cheese course information by season or episode ID.
38    pub async fn info(&self, params: CheeseInfoParams) -> BpiResult<CourseInfo> {
39        self.client
40            .get(INFO_ENDPOINT)
41            .query(&params.query_pairs())
42            .send_bpi_payload("cheese.info")
43            .await
44    }
45
46    /// Gets cheese course information by season ID.
47    pub async fn info_by_season_id(&self, season_id: SeasonId) -> BpiResult<CourseInfo> {
48        self.client
49            .get(INFO_ENDPOINT)
50            .query(&CheeseInfoParams::from_season_id(season_id).query_pairs())
51            .send_bpi_payload("cheese.info.season_detail_by_season_id")
52            .await
53    }
54
55    /// Gets cheese course information by episode ID.
56    pub async fn info_by_ep_id(&self, episode_id: EpisodeId) -> BpiResult<CourseInfo> {
57        self.client
58            .get(INFO_ENDPOINT)
59            .query(&CheeseInfoParams::from_episode_id(episode_id).query_pairs())
60            .send_bpi_payload("cheese.info.season_detail_by_ep_id")
61            .await
62    }
63
64    /// Gets a cheese course episode list.
65    pub async fn ep_list(&self, params: CheeseEpListParams) -> BpiResult<CourseEpList> {
66        self.client
67            .get(EP_LIST_ENDPOINT)
68            .query(&params.query_pairs())
69            .send_bpi_payload("cheese.info.ep_list")
70            .await
71    }
72
73    /// Gets cheese course video stream data.
74    pub async fn video_stream(
75        &self,
76        params: CheeseVideoStreamParams,
77    ) -> BpiResult<CourseVideoStreamData> {
78        self.client
79            .get(VIDEO_STREAM_ENDPOINT)
80            .query(&params.query_pairs())
81            .send_bpi_payload("cheese.playurl")
82            .await
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use std::future::Future;
89
90    use crate::cheese::info::{CourseEpList, CourseInfo};
91    use crate::cheese::videostream_url::CourseVideoStreamData;
92    use crate::cheese::{CheeseEpListParams, CheeseInfoParams, CheeseVideoStreamParams};
93    use crate::ids::{Aid, Cid, EpisodeId, SeasonId};
94    use crate::models::{Fnval, VideoQuality};
95    use crate::probe::contract::HttpMethod;
96    use crate::probe::endpoint_contract::EndpointContract;
97    use crate::{BpiClient, BpiResult};
98
99    const TEST_SEASON_ID: u64 = 556;
100    const TEST_EP_ID: u64 = 20767;
101    const TEST_AVID: u64 = 997984154;
102    const TEST_PLAYURL_EP_ID: u64 = 163956;
103    const TEST_CID: u64 = 1183682680;
104
105    fn season_id() -> BpiResult<SeasonId> {
106        SeasonId::new(TEST_SEASON_ID)
107    }
108
109    fn episode_id() -> BpiResult<EpisodeId> {
110        EpisodeId::new(TEST_EP_ID)
111    }
112
113    fn playurl_params() -> BpiResult<CheeseVideoStreamParams> {
114        Ok(CheeseVideoStreamParams::new(
115            Aid::new(TEST_AVID)?,
116            EpisodeId::new(TEST_PLAYURL_EP_ID)?,
117            Cid::new(TEST_CID)?,
118        )
119        .with_quality(VideoQuality::P480)
120        .with_fnval(Fnval::DASH))
121    }
122
123    fn assert_info_future<F>(_future: F)
124    where
125        F: Future<Output = BpiResult<CourseInfo>>,
126    {
127    }
128
129    fn assert_ep_list_future<F>(_future: F)
130    where
131        F: Future<Output = BpiResult<CourseEpList>>,
132    {
133    }
134
135    fn assert_video_stream_future<F>(_future: F)
136    where
137        F: Future<Output = BpiResult<CourseVideoStreamData>>,
138    {
139    }
140
141    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
142        let bytes = match endpoint {
143            "season-detail-season" => include_bytes!(
144                "../../tests/contracts/cheese/info/season-detail-season/contract.json"
145            )
146            .as_slice(),
147            "season-detail-episode" => include_bytes!(
148                "../../tests/contracts/cheese/info/season-detail-episode/contract.json"
149            )
150            .as_slice(),
151            "ep-list" => {
152                include_bytes!("../../tests/contracts/cheese/info/ep-list/contract.json").as_slice()
153            }
154            "playurl" => {
155                include_bytes!("../../tests/contracts/cheese/playurl/contract.json").as_slice()
156            }
157            _ => unreachable!("unknown cheese contract"),
158        };
159        EndpointContract::from_slice(bytes)
160    }
161
162    #[test]
163    fn cheese_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
164        let client = BpiClient::new()?;
165        let cheese = client.cheese();
166
167        assert_eq!(
168            cheese.info_endpoint(),
169            "https://api.bilibili.com/pugv/view/web/season"
170        );
171        assert_eq!(
172            cheese.ep_list_endpoint(),
173            "https://api.bilibili.com/pugv/view/web/ep/list"
174        );
175        assert_eq!(
176            cheese.video_stream_endpoint(),
177            "https://api.bilibili.com/pugv/player/web/playurl"
178        );
179        Ok(())
180    }
181
182    #[test]
183    fn cheese_methods_return_payload_futures() -> BpiResult<()> {
184        let client = BpiClient::new()?;
185        let cheese = client.cheese();
186
187        assert_info_future(cheese.info(CheeseInfoParams::from_season_id(season_id()?)));
188        assert_info_future(cheese.info_by_season_id(season_id()?));
189        assert_info_future(cheese.info_by_ep_id(episode_id()?));
190        assert_ep_list_future(
191            cheese.ep_list(
192                CheeseEpListParams::new(season_id()?)
193                    .with_page_size(50)?
194                    .with_page(1)?,
195            ),
196        );
197        assert_video_stream_future(cheese.video_stream(playurl_params()?));
198        Ok(())
199    }
200
201    #[test]
202    fn cheese_contracts_match_module_client_endpoints() -> BpiResult<()> {
203        let client = BpiClient::new()?;
204        let cheese = client.cheese();
205        let season = contract("season-detail-season")?;
206        let episode = contract("season-detail-episode")?;
207        let ep_list = contract("ep-list")?;
208        let playurl = contract("playurl")?;
209
210        assert_eq!(season.name, "cheese.info.season_detail_by_season_id");
211        assert_eq!(season.request.method, HttpMethod::Get);
212        assert_eq!(season.request.url.as_str(), cheese.info_endpoint());
213        assert_eq!(
214            season.request.query.get("season_id").map(String::as_str),
215            Some("556")
216        );
217
218        assert_eq!(episode.name, "cheese.info.season_detail_by_ep_id");
219        assert_eq!(episode.request.method, HttpMethod::Get);
220        assert_eq!(episode.request.url.as_str(), cheese.info_endpoint());
221        assert_eq!(
222            episode.request.query.get("ep_id").map(String::as_str),
223            Some("20767")
224        );
225
226        assert_eq!(ep_list.name, "cheese.info.ep_list");
227        assert_eq!(ep_list.request.method, HttpMethod::Get);
228        assert_eq!(ep_list.request.url.as_str(), cheese.ep_list_endpoint());
229
230        assert_eq!(playurl.name, "cheese.playurl");
231        assert_eq!(playurl.request.method, HttpMethod::Get);
232        assert_eq!(playurl.request.url.as_str(), cheese.video_stream_endpoint());
233        assert_eq!(
234            playurl.request.query.get("fnval").map(String::as_str),
235            Some("16")
236        );
237        Ok(())
238    }
239}