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#[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 pub async fn info(&self, params: CheeseInfoParams) -> BpiResult<CourseInfo> {
39 self.client
40 .get(INFO_ENDPOINT)
41 .query(¶ms.query_pairs())
42 .send_bpi_payload("cheese.info")
43 .await
44 }
45
46 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 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 pub async fn ep_list(&self, params: CheeseEpListParams) -> BpiResult<CourseEpList> {
66 self.client
67 .get(EP_LIST_ENDPOINT)
68 .query(¶ms.query_pairs())
69 .send_bpi_payload("cheese.info.ep_list")
70 .await
71 }
72
73 pub async fn video_stream(
75 &self,
76 params: CheeseVideoStreamParams,
77 ) -> BpiResult<CourseVideoStreamData> {
78 self.client
79 .get(VIDEO_STREAM_ENDPOINT)
80 .query(¶ms.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}