1use crate::search::hot::{DefaultSearchData, HotWordDataResponse};
2use crate::search::result::{
3 Article, Bangumi, BiliUser, LiveData, LiveRoom, LiveUser, Movie, SearchData, Video,
4};
5use crate::search::search_params::{
6 SearchArticleParams, SearchBangumiParams, SearchBiliUserParams, SearchLiveParams,
7 SearchLiveRoomParams, SearchLiveUserParams, SearchMovieParams, SearchVideoParams,
8};
9use crate::search::suggest::{SearchSuggest, SearchSuggestParams};
10use crate::{BilibiliRequest, BpiClient, BpiResult};
11
12const TYPED_ENDPOINT: &str = "https://api.bilibili.com/x/web-interface/wbi/search/type";
13const DEFAULT_ENDPOINT: &str = "https://api.bilibili.com/x/web-interface/wbi/search/default";
14const SUGGEST_ENDPOINT: &str = "https://s.search.bilibili.com/main/suggest";
15const HOTWORDS_ENDPOINT: &str = "https://s.search.bilibili.com/main/hotword";
16
17#[derive(Clone, Copy)]
19pub struct SearchClient<'a> {
20 pub(crate) client: &'a BpiClient,
21}
22
23impl<'a> SearchClient<'a> {
24 pub(crate) fn new(client: &'a BpiClient) -> Self {
25 Self { client }
26 }
27
28 #[cfg(test)]
29 pub(crate) fn typed_endpoint(&self) -> &'static str {
30 TYPED_ENDPOINT
31 }
32
33 #[cfg(test)]
34 pub(crate) fn default_endpoint(&self) -> &'static str {
35 DEFAULT_ENDPOINT
36 }
37
38 #[cfg(test)]
39 pub(crate) fn suggest_endpoint(&self) -> &'static str {
40 SUGGEST_ENDPOINT
41 }
42
43 #[cfg(test)]
44 pub(crate) fn hotwords_endpoint(&self) -> &'static str {
45 HOTWORDS_ENDPOINT
46 }
47
48 pub async fn article(
50 &self,
51 params: SearchArticleParams,
52 ) -> BpiResult<SearchData<Vec<Article>>> {
53 self.typed_search(params.query_pairs(), "search.article")
54 .await
55 }
56
57 pub async fn bangumi(
59 &self,
60 params: SearchBangumiParams,
61 ) -> BpiResult<SearchData<Vec<Bangumi>>> {
62 self.typed_search(params.query_pairs(), "search.bangumi")
63 .await
64 }
65
66 pub async fn bili_user(
68 &self,
69 params: SearchBiliUserParams,
70 ) -> BpiResult<SearchData<Vec<BiliUser>>> {
71 self.typed_search(params.query_pairs(), "search.bili_user")
72 .await
73 }
74
75 pub async fn live(&self, params: SearchLiveParams) -> BpiResult<SearchData<LiveData>> {
77 self.typed_search(params.query_pairs(), "search.live").await
78 }
79
80 pub async fn live_room(
82 &self,
83 params: SearchLiveRoomParams,
84 ) -> BpiResult<SearchData<Vec<LiveRoom>>> {
85 self.typed_search(params.query_pairs(), "search.live_room")
86 .await
87 }
88
89 pub async fn live_user(
91 &self,
92 params: SearchLiveUserParams,
93 ) -> BpiResult<SearchData<Vec<LiveUser>>> {
94 self.typed_search(params.query_pairs(), "search.live_user")
95 .await
96 }
97
98 pub async fn movie(&self, params: SearchMovieParams) -> BpiResult<SearchData<Vec<Movie>>> {
100 self.typed_search(params.query_pairs(), "search.movie")
101 .await
102 }
103
104 pub async fn video(&self, params: SearchVideoParams) -> BpiResult<SearchData<Vec<Video>>> {
106 self.typed_search(params.query_pairs(), "search.video")
107 .await
108 }
109
110 pub async fn default(&self) -> BpiResult<DefaultSearchData> {
112 let signed_params = self.client.get_wbi_sign2(vec![("foo", "bar")]).await?;
113
114 self.client
115 .get(DEFAULT_ENDPOINT)
116 .query(&signed_params)
117 .send_bpi_payload("search.default")
118 .await
119 }
120
121 pub async fn suggest(&self, params: SearchSuggestParams) -> BpiResult<SearchSuggest> {
123 self.client
124 .get(SUGGEST_ENDPOINT)
125 .query(¶ms.query_pairs())
126 .send_bpi_payload("search.suggest")
127 .await
128 }
129
130 pub async fn hotwords(&self) -> BpiResult<HotWordDataResponse> {
132 let response = self.client.get(HOTWORDS_ENDPOINT).send().await?;
133
134 Ok(response.json().await?)
135 }
136
137 async fn typed_search<T>(
138 &self,
139 query_pairs: Vec<(&'static str, String)>,
140 endpoint_label: &'static str,
141 ) -> BpiResult<SearchData<T>>
142 where
143 T: serde::de::DeserializeOwned,
144 {
145 let signed_params = self.client.get_wbi_sign2(query_pairs).await?;
146
147 self.client
148 .get(TYPED_ENDPOINT)
149 .query(&signed_params)
150 .send_bpi_payload(endpoint_label)
151 .await
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use std::future::Future;
158
159 use crate::probe::contract::HttpMethod;
160 use crate::probe::endpoint_contract::EndpointContract;
161 use crate::search::hot::{DefaultSearchData, HotWordDataResponse};
162 use crate::search::result::{
163 Article, Bangumi, BiliUser, LiveData, LiveRoom, LiveUser, Movie, SearchData, Video,
164 };
165 use crate::search::search_params::{
166 SearchArticleParams, SearchBangumiParams, SearchBiliUserParams, SearchLiveParams,
167 SearchLiveRoomParams, SearchLiveUserParams, SearchMovieParams, SearchVideoParams,
168 };
169 use crate::search::suggest::{SearchSuggest, SearchSuggestParams};
170 use crate::{BpiClient, BpiResult};
171
172 fn assert_article_future<F>(_future: F)
173 where
174 F: Future<Output = BpiResult<SearchData<Vec<Article>>>>,
175 {
176 }
177
178 fn assert_bangumi_future<F>(_future: F)
179 where
180 F: Future<Output = BpiResult<SearchData<Vec<Bangumi>>>>,
181 {
182 }
183
184 fn assert_bili_user_future<F>(_future: F)
185 where
186 F: Future<Output = BpiResult<SearchData<Vec<BiliUser>>>>,
187 {
188 }
189
190 fn assert_live_future<F>(_future: F)
191 where
192 F: Future<Output = BpiResult<SearchData<LiveData>>>,
193 {
194 }
195
196 fn assert_live_room_future<F>(_future: F)
197 where
198 F: Future<Output = BpiResult<SearchData<Vec<LiveRoom>>>>,
199 {
200 }
201
202 fn assert_live_user_future<F>(_future: F)
203 where
204 F: Future<Output = BpiResult<SearchData<Vec<LiveUser>>>>,
205 {
206 }
207
208 fn assert_movie_future<F>(_future: F)
209 where
210 F: Future<Output = BpiResult<SearchData<Vec<Movie>>>>,
211 {
212 }
213
214 fn assert_video_future<F>(_future: F)
215 where
216 F: Future<Output = BpiResult<SearchData<Vec<Video>>>>,
217 {
218 }
219
220 fn assert_default_future<F>(_future: F)
221 where
222 F: Future<Output = BpiResult<DefaultSearchData>>,
223 {
224 }
225
226 fn assert_suggest_future<F>(_future: F)
227 where
228 F: Future<Output = BpiResult<SearchSuggest>>,
229 {
230 }
231
232 fn assert_hotwords_future<F>(_future: F)
233 where
234 F: Future<Output = BpiResult<HotWordDataResponse>>,
235 {
236 }
237
238 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
239 let bytes = match endpoint {
240 "article" => {
241 include_bytes!("../../tests/contracts/search/read/article/contract.json").as_slice()
242 }
243 "bangumi" => {
244 include_bytes!("../../tests/contracts/search/read/bangumi/contract.json").as_slice()
245 }
246 "bili-user" => {
247 include_bytes!("../../tests/contracts/search/read/bili-user/contract.json")
248 .as_slice()
249 }
250 "live" => {
251 include_bytes!("../../tests/contracts/search/read/live/contract.json").as_slice()
252 }
253 "live-room" => {
254 include_bytes!("../../tests/contracts/search/read/live-room/contract.json")
255 .as_slice()
256 }
257 "live-user" => {
258 include_bytes!("../../tests/contracts/search/read/live-user/contract.json")
259 .as_slice()
260 }
261 "movie" => {
262 include_bytes!("../../tests/contracts/search/read/movie/contract.json").as_slice()
263 }
264 "video" => {
265 include_bytes!("../../tests/contracts/search/read/video/contract.json").as_slice()
266 }
267 "default" => {
268 include_bytes!("../../tests/contracts/search/read/default/contract.json").as_slice()
269 }
270 "suggest" => {
271 include_bytes!("../../tests/contracts/search/read/suggest/contract.json").as_slice()
272 }
273 "hotwords" => {
274 include_bytes!("../../tests/contracts/search/read/hotwords/contract.json")
275 .as_slice()
276 }
277 _ => unreachable!("unknown search contract"),
278 };
279 EndpointContract::from_slice(bytes)
280 }
281
282 #[test]
283 fn search_client_exposes_promoted_endpoint_urls() -> BpiResult<()> {
284 let client = BpiClient::new()?;
285 let search = client.search();
286
287 assert_eq!(
288 search.typed_endpoint(),
289 "https://api.bilibili.com/x/web-interface/wbi/search/type"
290 );
291 assert_eq!(
292 search.default_endpoint(),
293 "https://api.bilibili.com/x/web-interface/wbi/search/default"
294 );
295 assert_eq!(
296 search.suggest_endpoint(),
297 "https://s.search.bilibili.com/main/suggest"
298 );
299 assert_eq!(
300 search.hotwords_endpoint(),
301 "https://s.search.bilibili.com/main/hotword"
302 );
303 Ok(())
304 }
305
306 #[test]
307 fn search_methods_return_payload_futures() -> BpiResult<()> {
308 let client = BpiClient::new()?;
309 let search = client.search();
310
311 assert_article_future(search.article(SearchArticleParams::new("rust")?));
312 assert_bangumi_future(search.bangumi(SearchBangumiParams::new("天气之子")?));
313 assert_bili_user_future(search.bili_user(SearchBiliUserParams::new("老番茄")?));
314 assert_live_future(search.live(SearchLiveParams::new("游戏")?));
315 assert_live_room_future(search.live_room(SearchLiveRoomParams::new("游戏")?));
316 assert_live_user_future(search.live_user(SearchLiveUserParams::new("散人")?));
317 assert_movie_future(search.movie(SearchMovieParams::new("哈利波特")?));
318 assert_video_future(search.video(SearchVideoParams::new("rust")?));
319 assert_default_future(search.default());
320 assert_suggest_future(search.suggest(SearchSuggestParams::new("rust")?));
321 assert_hotwords_future(search.hotwords());
322 Ok(())
323 }
324
325 #[test]
326 fn search_contracts_match_module_client_endpoints() -> BpiResult<()> {
327 let client = BpiClient::new()?;
328 let search = client.search();
329
330 let typed_expectations = [
331 ("article", "search.article"),
332 ("bangumi", "search.bangumi"),
333 ("bili-user", "search.bili_user"),
334 ("live", "search.live"),
335 ("live-room", "search.live_room"),
336 ("live-user", "search.live_user"),
337 ("movie", "search.movie"),
338 ("video", "search.video"),
339 ];
340
341 for (endpoint, name) in typed_expectations {
342 let contract = contract(endpoint)?;
343
344 assert_eq!(contract.name, name);
345 assert_eq!(contract.request.method, HttpMethod::Get);
346 assert_eq!(contract.request.url.as_str(), search.typed_endpoint());
347 assert!(contract.request.auth.requires_wbi());
348 }
349
350 let default = contract("default")?;
351 assert_eq!(default.name, "search.default");
352 assert_eq!(default.request.method, HttpMethod::Get);
353 assert_eq!(default.request.url.as_str(), search.default_endpoint());
354 assert!(default.request.auth.requires_wbi());
355
356 let suggest = contract("suggest")?;
357 assert_eq!(suggest.name, "search.suggest");
358 assert_eq!(suggest.request.method, HttpMethod::Get);
359 assert_eq!(suggest.request.url.as_str(), search.suggest_endpoint());
360 assert!(!suggest.request.auth.requires_wbi());
361
362 let hotwords = contract("hotwords")?;
363 assert_eq!(hotwords.name, "search.hotwords");
364 assert_eq!(hotwords.request.method, HttpMethod::Get);
365 assert_eq!(hotwords.request.url.as_str(), search.hotwords_endpoint());
366 assert!(!hotwords.request.auth.requires_wbi());
367 Ok(())
368 }
369}