1use crate::fav::info::{
2 CollectedFolderListData, CreatedFolderListData, FavFolderInfo, ResourceInfoItem,
3};
4use crate::fav::list::{FavListDetailData, FavResourceIdItem};
5use crate::fav::{
6 FavCollectedListParams, FavCreatedListParams, FavFolderInfoParams, FavListDetailParams,
7 FavResourceIdsParams, FavResourceInfosParams,
8};
9use crate::{BilibiliRequest, BpiClient, BpiResult};
10
11const FOLDER_INFO_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/folder/info";
12const CREATED_LIST_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/folder/created/list-all";
13const COLLECTED_LIST_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/folder/collected/list";
14const RESOURCE_INFOS_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/resource/infos";
15const LIST_DETAIL_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/resource/list";
16const RESOURCE_IDS_ENDPOINT: &str = "https://api.bilibili.com/x/v3/fav/resource/ids";
17
18#[derive(Clone, Copy)]
20pub struct FavClient<'a> {
21 pub(crate) client: &'a BpiClient,
22}
23
24impl<'a> FavClient<'a> {
25 pub(crate) fn new(client: &'a BpiClient) -> Self {
26 Self { client }
27 }
28
29 #[cfg(test)]
30 pub(crate) fn folder_info_endpoint(&self) -> &'static str {
31 FOLDER_INFO_ENDPOINT
32 }
33
34 #[cfg(test)]
35 pub(crate) fn created_list_endpoint(&self) -> &'static str {
36 CREATED_LIST_ENDPOINT
37 }
38
39 #[cfg(test)]
40 pub(crate) fn collected_list_endpoint(&self) -> &'static str {
41 COLLECTED_LIST_ENDPOINT
42 }
43
44 #[cfg(test)]
45 pub(crate) fn resource_infos_endpoint(&self) -> &'static str {
46 RESOURCE_INFOS_ENDPOINT
47 }
48
49 #[cfg(test)]
50 pub(crate) fn list_detail_endpoint(&self) -> &'static str {
51 LIST_DETAIL_ENDPOINT
52 }
53
54 #[cfg(test)]
55 pub(crate) fn resource_ids_endpoint(&self) -> &'static str {
56 RESOURCE_IDS_ENDPOINT
57 }
58
59 pub async fn folder_info(&self, params: FavFolderInfoParams) -> BpiResult<FavFolderInfo> {
61 self.client
62 .get(FOLDER_INFO_ENDPOINT)
63 .query(¶ms.query_pairs())
64 .send_bpi_payload("fav.folder_info")
65 .await
66 }
67
68 pub async fn created_list(
70 &self,
71 params: FavCreatedListParams,
72 ) -> BpiResult<CreatedFolderListData> {
73 self.client
74 .get(CREATED_LIST_ENDPOINT)
75 .query(¶ms.query_pairs())
76 .send_bpi_payload("fav.created_list")
77 .await
78 }
79
80 pub async fn collected_list(
82 &self,
83 params: FavCollectedListParams,
84 ) -> BpiResult<CollectedFolderListData> {
85 self.client
86 .get(COLLECTED_LIST_ENDPOINT)
87 .query(¶ms.query_pairs())
88 .send_bpi_payload("fav.collected_list")
89 .await
90 }
91
92 pub async fn resource_infos(
94 &self,
95 params: FavResourceInfosParams,
96 ) -> BpiResult<Vec<ResourceInfoItem>> {
97 self.client
98 .get(RESOURCE_INFOS_ENDPOINT)
99 .query(¶ms.query_pairs())
100 .send_bpi_payload("fav.resource_infos")
101 .await
102 }
103
104 pub async fn list_detail(&self, params: FavListDetailParams) -> BpiResult<FavListDetailData> {
106 self.client
107 .get(LIST_DETAIL_ENDPOINT)
108 .query(¶ms.query_pairs())
109 .send_bpi_payload("fav.list_detail")
110 .await
111 }
112
113 pub async fn resource_ids(
115 &self,
116 params: FavResourceIdsParams,
117 ) -> BpiResult<Vec<FavResourceIdItem>> {
118 self.client
119 .get(RESOURCE_IDS_ENDPOINT)
120 .query(¶ms.query_pairs())
121 .send_bpi_payload("fav.resource_ids")
122 .await
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use std::future::Future;
129
130 use crate::fav::info::{
131 CollectedFolderListData, CreatedFolderListData, FavFolderInfo, ResourceInfoItem,
132 };
133 use crate::fav::list::{FavListDetailData, FavResourceIdItem};
134 use crate::fav::{
135 FavCollectedListParams, FavCreatedListParams, FavFolderInfoParams, FavListDetailParams,
136 FavResourceIdsParams, FavResourceInfosParams,
137 };
138 use crate::ids::{MediaId, Mid};
139 use crate::probe::contract::HttpMethod;
140 use crate::probe::endpoint_contract::EndpointContract;
141 use crate::{BpiClient, BpiResult};
142
143 const TEST_MEDIA_ID: u64 = 1_052_622_027;
144 const TEST_MID: u64 = 7_792_521;
145
146 fn media_id() -> BpiResult<MediaId> {
147 MediaId::new(TEST_MEDIA_ID)
148 }
149
150 fn mid() -> BpiResult<Mid> {
151 Mid::new(TEST_MID)
152 }
153
154 fn assert_folder_info_future<F>(_future: F)
155 where
156 F: Future<Output = BpiResult<FavFolderInfo>>,
157 {
158 }
159
160 fn assert_created_list_future<F>(_future: F)
161 where
162 F: Future<Output = BpiResult<CreatedFolderListData>>,
163 {
164 }
165
166 fn assert_collected_list_future<F>(_future: F)
167 where
168 F: Future<Output = BpiResult<CollectedFolderListData>>,
169 {
170 }
171
172 fn assert_resource_infos_future<F>(_future: F)
173 where
174 F: Future<Output = BpiResult<Vec<ResourceInfoItem>>>,
175 {
176 }
177
178 fn assert_list_detail_future<F>(_future: F)
179 where
180 F: Future<Output = BpiResult<FavListDetailData>>,
181 {
182 }
183
184 fn assert_resource_ids_future<F>(_future: F)
185 where
186 F: Future<Output = BpiResult<Vec<FavResourceIdItem>>>,
187 {
188 }
189
190 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
191 let bytes = match endpoint {
192 "folder-info" => {
193 include_bytes!("../../tests/contracts/fav/read/folder-info/contract.json")
194 .as_slice()
195 }
196 "created-list" => {
197 include_bytes!("../../tests/contracts/fav/read/created-list/contract.json")
198 .as_slice()
199 }
200 "collected-list" => {
201 include_bytes!("../../tests/contracts/fav/read/collected-list/contract.json")
202 .as_slice()
203 }
204 "resource-infos" => {
205 include_bytes!("../../tests/contracts/fav/read/resource-infos/contract.json")
206 .as_slice()
207 }
208 "list-detail" => {
209 include_bytes!("../../tests/contracts/fav/read/list-detail/contract.json")
210 .as_slice()
211 }
212 "resource-ids" => {
213 include_bytes!("../../tests/contracts/fav/read/resource-ids/contract.json")
214 .as_slice()
215 }
216 _ => unreachable!("unknown fav read contract"),
217 };
218 EndpointContract::from_slice(bytes)
219 }
220
221 #[test]
222 fn fav_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
223 let client = BpiClient::new()?;
224 let fav = client.fav();
225
226 assert_eq!(
227 fav.folder_info_endpoint(),
228 "https://api.bilibili.com/x/v3/fav/folder/info"
229 );
230 assert_eq!(
231 fav.created_list_endpoint(),
232 "https://api.bilibili.com/x/v3/fav/folder/created/list-all"
233 );
234 assert_eq!(
235 fav.collected_list_endpoint(),
236 "https://api.bilibili.com/x/v3/fav/folder/collected/list"
237 );
238 assert_eq!(
239 fav.resource_infos_endpoint(),
240 "https://api.bilibili.com/x/v3/fav/resource/infos"
241 );
242 assert_eq!(
243 fav.list_detail_endpoint(),
244 "https://api.bilibili.com/x/v3/fav/resource/list"
245 );
246 assert_eq!(
247 fav.resource_ids_endpoint(),
248 "https://api.bilibili.com/x/v3/fav/resource/ids"
249 );
250 Ok(())
251 }
252
253 #[test]
254 fn fav_methods_return_payload_futures() -> BpiResult<()> {
255 let client = BpiClient::new()?;
256 let fav = client.fav();
257
258 assert_folder_info_future(fav.folder_info(FavFolderInfoParams::new(media_id()?)));
259 assert_created_list_future(fav.created_list(FavCreatedListParams::new(mid()?)));
260 assert_collected_list_future(fav.collected_list(FavCollectedListParams::new(mid()?)));
261 assert_resource_infos_future(
262 fav.resource_infos(FavResourceInfosParams::new("371494037:2")?),
263 );
264 assert_list_detail_future(
265 fav.list_detail(
266 FavListDetailParams::new(media_id()?)
267 .order("mtime")?
268 .content_type(0)
269 .page_size(5)?
270 .page(1)?,
271 ),
272 );
273 assert_resource_ids_future(fav.resource_ids(FavResourceIdsParams::new(media_id()?)));
274 Ok(())
275 }
276
277 #[test]
278 fn fav_contracts_match_module_client_endpoints() -> BpiResult<()> {
279 let client = BpiClient::new()?;
280 let fav = client.fav();
281 let folder_info = contract("folder-info")?;
282 let created_list = contract("created-list")?;
283 let collected_list = contract("collected-list")?;
284 let resource_infos = contract("resource-infos")?;
285 let list_detail = contract("list-detail")?;
286 let resource_ids = contract("resource-ids")?;
287
288 assert_eq!(folder_info.name, "fav.folder_info");
289 assert_eq!(folder_info.request.method, HttpMethod::Get);
290 assert_eq!(folder_info.request.url.as_str(), fav.folder_info_endpoint());
291
292 assert_eq!(created_list.name, "fav.created_list");
293 assert_eq!(created_list.request.method, HttpMethod::Get);
294 assert_eq!(
295 created_list.request.url.as_str(),
296 fav.created_list_endpoint()
297 );
298
299 assert_eq!(collected_list.name, "fav.collected_list");
300 assert_eq!(collected_list.request.method, HttpMethod::Get);
301 assert_eq!(
302 collected_list.request.url.as_str(),
303 fav.collected_list_endpoint()
304 );
305
306 assert_eq!(resource_infos.name, "fav.resource_infos");
307 assert_eq!(resource_infos.request.method, HttpMethod::Get);
308 assert_eq!(
309 resource_infos.request.url.as_str(),
310 fav.resource_infos_endpoint()
311 );
312
313 assert_eq!(list_detail.name, "fav.list_detail");
314 assert_eq!(list_detail.request.method, HttpMethod::Get);
315 assert_eq!(list_detail.request.url.as_str(), fav.list_detail_endpoint());
316
317 assert_eq!(resource_ids.name, "fav.resource_ids");
318 assert_eq!(resource_ids.request.method, HttpMethod::Get);
319 assert_eq!(
320 resource_ids.request.url.as_str(),
321 fav.resource_ids_endpoint()
322 );
323 assert_eq!(
324 resource_ids
325 .request
326 .query
327 .get("platform")
328 .map(String::as_str),
329 Some("web")
330 );
331 Ok(())
332 }
333}