Skip to main content

bpi_rs/fav/
client.rs

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/// Favorite API client.
19#[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    /// Gets favorite folder metadata.
60    pub async fn folder_info(&self, params: FavFolderInfoParams) -> BpiResult<FavFolderInfo> {
61        self.client
62            .get(FOLDER_INFO_ENDPOINT)
63            .query(&params.query_pairs())
64            .send_bpi_payload("fav.folder_info")
65            .await
66    }
67
68    /// Gets folders created by a user.
69    pub async fn created_list(
70        &self,
71        params: FavCreatedListParams,
72    ) -> BpiResult<CreatedFolderListData> {
73        self.client
74            .get(CREATED_LIST_ENDPOINT)
75            .query(&params.query_pairs())
76            .send_bpi_payload("fav.created_list")
77            .await
78    }
79
80    /// Gets folders collected by a user.
81    pub async fn collected_list(
82        &self,
83        params: FavCollectedListParams,
84    ) -> BpiResult<CollectedFolderListData> {
85        self.client
86            .get(COLLECTED_LIST_ENDPOINT)
87            .query(&params.query_pairs())
88            .send_bpi_payload("fav.collected_list")
89            .await
90    }
91
92    /// Gets resource information for multiple favorite resources.
93    pub async fn resource_infos(
94        &self,
95        params: FavResourceInfosParams,
96    ) -> BpiResult<Vec<ResourceInfoItem>> {
97        self.client
98            .get(RESOURCE_INFOS_ENDPOINT)
99            .query(&params.query_pairs())
100            .send_bpi_payload("fav.resource_infos")
101            .await
102    }
103
104    /// Gets a favorite folder's detailed resource list.
105    pub async fn list_detail(&self, params: FavListDetailParams) -> BpiResult<FavListDetailData> {
106        self.client
107            .get(LIST_DETAIL_ENDPOINT)
108            .query(&params.query_pairs())
109            .send_bpi_payload("fav.list_detail")
110            .await
111    }
112
113    /// Gets all resource IDs in a favorite folder.
114    pub async fn resource_ids(
115        &self,
116        params: FavResourceIdsParams,
117    ) -> BpiResult<Vec<FavResourceIdItem>> {
118        self.client
119            .get(RESOURCE_IDS_ENDPOINT)
120            .query(&params.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}