pexels_sdk/
client.rs

1use reqwest::{header, Client, StatusCode};
2use std::time::Duration;
3use url::Url;
4
5use crate::models::{CollectionsPage, MediaPage, Photo, PhotosPage, Video, VideosPage};
6use crate::search::{PaginationParams, SearchParams, VideoSearchParams};
7use crate::PexelsError;
8
9/// Pexels API 的主要客户端
10///
11/// 此客户端提供与 Pexels API 所有端点交互的方法,
12/// 并处理认证、请求构建和响应解析。
13pub struct PexelsClient {
14    /// 用于 Pexels API 认证的 API 密钥
15    api_key: String,
16
17    /// 具有连接池和可配置超时的 HTTP 客户端
18    client: Client,
19
20    /// Pexels API 的基础 URL
21    base_url: String,
22}
23
24impl PexelsClient {
25    /// 使用提供的 API 密钥创建新的 PexelsClient
26    ///
27    /// # 参数
28    ///
29    /// * `api_key` - Pexels API 密钥
30    ///
31    /// # 返回
32    ///
33    /// PexelsClient 的新实例
34    ///
35    /// # 示例
36    ///
37    /// ```
38    /// use pexels_sdk::PexelsClient;
39    ///
40    /// let client = PexelsClient::new("your_api_key");
41    /// ```
42    pub fn new<S: Into<String>>(api_key: S) -> Self {
43        let client = Client::builder()
44            .timeout(Duration::from_secs(30))
45            .pool_max_idle_per_host(10)
46            .build()
47            .unwrap_or_default();
48
49        Self {
50            api_key: api_key.into(),
51            client,
52            base_url: "https://api.pexels.com/v1".to_string(),
53        }
54    }
55
56    /// 使用自定义配置创建新的 PexelsClient
57    ///
58    /// # 参数
59    ///
60    /// * `api_key` - Pexels API 密钥
61    /// * `timeout` - 请求超时时间(秒)
62    /// * `max_idle_connections` - 每个主机的最大空闲连接数
63    ///
64    /// # 返回
65    ///
66    /// PexelsClient 的新实例
67    pub fn with_config<S: Into<String>>(
68        api_key: S,
69        timeout: u64,
70        max_idle_connections: usize,
71    ) -> Self {
72        let client = Client::builder()
73            .timeout(Duration::from_secs(timeout))
74            .pool_max_idle_per_host(max_idle_connections)
75            .build()
76            .unwrap_or_default();
77
78        Self {
79            api_key: api_key.into(),
80            client,
81            base_url: "https://api.pexels.com/v1".to_string(),
82        }
83    }
84
85    /// 为 Pexels API 设置自定义基础 URL
86    ///
87    /// # 参数
88    ///
89    /// * `base_url` - 自定义基础 URL
90    ///
91    /// # 返回
92    ///
93    /// 用于方法链的 Self
94    pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
95        self.base_url = base_url.into();
96        self
97    }
98
99    /// 搜索与指定查询和参数匹配的照片
100    ///
101    /// # 参数
102    ///
103    /// * `query` - 搜索查询
104    /// * `params` - 其他搜索参数(分页、过滤器等)
105    ///
106    /// # 返回
107    ///
108    /// 包含照片搜索响应或错误的结果
109    ///
110    /// # 示例
111    ///
112    /// ```
113    /// use pexels_sdk::{PexelsClient, SearchParams, Size};
114    ///
115    /// #[tokio::main]
116    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
117    ///     let client = PexelsClient::new("your_api_key");
118    ///     let params = SearchParams::new()
119    ///         .page(1)
120    ///         .per_page(15)
121    ///         .size(Size::Large);
122    ///
123    ///     match client.search_photos("nature", &params).await {
124    ///         Ok(photos) => println!("Found {} photos", photos.total_results),
125    ///         Err(e) => println!("搜索失败: {}", e),
126    ///     }
127    ///     Ok(())
128    /// }
129    /// ```
130    pub async fn search_photos(
131        &self,
132        query: &str,
133        params: &SearchParams,
134    ) -> Result<PhotosPage, PexelsError> {
135        let mut url = Url::parse(&format!("{}/search", self.base_url))?;
136
137        // 添加查询参数
138        url.query_pairs_mut().append_pair("query", query);
139
140        // 添加所有搜索参数
141        for (key, value) in params.to_query_params() {
142            url.query_pairs_mut().append_pair(&key, &value);
143        }
144
145        let response = self.send_request(url).await?;
146
147        match response.status() {
148            StatusCode::OK => {
149                let photos_page: PhotosPage = response.json().await?;
150                Ok(photos_page)
151            }
152            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
153            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
154            status => Err(PexelsError::ApiError(format!(
155                "Search photos failed with status: {status}"
156            ))),
157        }
158    }
159
160    /// 获取精选/推荐照片
161    ///
162    /// # 参数
163    ///
164    /// * `params` - 分页参数
165    ///
166    /// # 返回
167    ///
168    /// 包含精选照片响应或错误的结果
169    pub async fn curated_photos(
170        &self,
171        params: &PaginationParams,
172    ) -> Result<PhotosPage, PexelsError> {
173        let mut url = Url::parse(&format!("{}/curated", self.base_url))?;
174
175        // 添加分页参数
176        if let Some(page) = params.page {
177            url.query_pairs_mut().append_pair("page", &page.to_string());
178        }
179
180        if let Some(per_page) = params.per_page {
181            url.query_pairs_mut()
182                .append_pair("per_page", &per_page.to_string());
183        }
184
185        let response = self.send_request(url).await?;
186
187        match response.status() {
188            StatusCode::OK => {
189                let photos_page: PhotosPage = response.json().await?;
190                Ok(photos_page)
191            }
192            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
193            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
194            status => Err(PexelsError::ApiError(format!(
195                "Curated photos failed with status: {status}"
196            ))),
197        }
198    }
199
200    /// 根据 ID 获取特定照片
201    ///
202    /// # 参数
203    ///
204    /// * `id` - 照片 ID
205    ///
206    /// # 返回
207    ///
208    /// 包含照片或错误的结果
209    pub async fn get_photo(&self, id: u64) -> Result<Photo, PexelsError> {
210        let url = Url::parse(&format!("{}/photos/{}", self.base_url, id))?;
211
212        let response = self.send_request(url).await?;
213
214        match response.status() {
215            StatusCode::OK => {
216                let photo: Photo = response.json().await?;
217                Ok(photo)
218            }
219            StatusCode::NOT_FOUND => Err(PexelsError::NotFound(format!(
220                "Photo with ID {id} not found"
221            ))),
222            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
223            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
224            status => Err(PexelsError::ApiError(format!(
225                "Get photo failed with status: {status}"
226            ))),
227        }
228    }
229
230    /// 搜索与指定查询和参数匹配的视频
231    ///
232    /// # 参数
233    ///
234    /// * `query` - 搜索查询
235    /// * `params` - 其他搜索参数(分页、过滤器等)
236    ///
237    /// # 返回
238    ///
239    /// 包含视频搜索响应或错误的结果
240    pub async fn search_videos(
241        &self,
242        query: &str,
243        params: &VideoSearchParams,
244    ) -> Result<VideosPage, PexelsError> {
245        let mut url = Url::parse(&format!("{}/videos/search", self.base_url))?;
246
247        // 添加查询参数
248        url.query_pairs_mut().append_pair("query", query);
249
250        // 添加所有来自 VideoSearchParams 的搜索参数
251        if let Some(page) = params.page {
252            url.query_pairs_mut().append_pair("page", &page.to_string());
253        }
254
255        if let Some(per_page) = params.per_page {
256            url.query_pairs_mut()
257                .append_pair("per_page", &per_page.to_string());
258        }
259
260        if let Some(ref orientation) = params.orientation {
261            url.query_pairs_mut()
262                .append_pair("orientation", orientation.as_str());
263        }
264
265        if let Some(ref size) = params.size {
266            url.query_pairs_mut().append_pair("size", size.as_str());
267        }
268
269        if let Some(ref locale) = params.locale {
270            url.query_pairs_mut().append_pair("locale", locale);
271        }
272
273        let response = self.send_request(url).await?;
274
275        match response.status() {
276            StatusCode::OK => {
277                let videos_page: VideosPage = response.json().await?;
278                Ok(videos_page)
279            }
280            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
281            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
282            status => Err(PexelsError::ApiError(format!(
283                "Search videos failed with status: {status}"
284            ))),
285        }
286    }
287
288    /// 获取热门视频
289    ///
290    /// # 参数
291    ///
292    /// * `params` - 分页参数
293    ///
294    /// # 返回
295    ///
296    /// 包含热门视频响应或错误的结果
297    pub async fn popular_videos(
298        &self,
299        params: &PaginationParams,
300    ) -> Result<VideosPage, PexelsError> {
301        let mut url = Url::parse(&format!("{}/videos/popular", self.base_url))?;
302
303        // 添加分页参数
304        if let Some(page) = params.page {
305            url.query_pairs_mut().append_pair("page", &page.to_string());
306        }
307
308        if let Some(per_page) = params.per_page {
309            url.query_pairs_mut()
310                .append_pair("per_page", &per_page.to_string());
311        }
312
313        let response = self.send_request(url).await?;
314
315        match response.status() {
316            StatusCode::OK => {
317                let videos_page: VideosPage = response.json().await?;
318                Ok(videos_page)
319            }
320            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
321            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
322            status => Err(PexelsError::ApiError(format!(
323                "Popular videos failed with status: {status}"
324            ))),
325        }
326    }
327
328    /// 根据 ID 获取特定视频
329    ///
330    /// # 参数
331    ///
332    /// * `id` - 视频 ID
333    ///
334    /// # 返回
335    ///
336    /// 包含视频或错误的结果
337    pub async fn get_video(&self, id: u64) -> Result<Video, PexelsError> {
338        let url = Url::parse(&format!("{}/videos/videos/{}", self.base_url, id))?;
339
340        let response = self.send_request(url).await?;
341
342        match response.status() {
343            StatusCode::OK => {
344                let video: Video = response.json().await?;
345                Ok(video)
346            }
347            StatusCode::NOT_FOUND => Err(PexelsError::NotFound(format!(
348                "Video with ID {id} not found"
349            ))),
350            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
351            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
352            status => Err(PexelsError::ApiError(format!(
353                "Get video failed with status: {status}"
354            ))),
355        }
356    }
357
358    /// 获取收藏列表
359    ///
360    /// # 参数
361    ///
362    /// * `params` - 分页参数
363    ///
364    /// # 返回
365    ///
366    /// 包含收藏响应或错误的结果
367    pub async fn get_collections(
368        &self,
369        params: &PaginationParams,
370    ) -> Result<CollectionsPage, PexelsError> {
371        let mut url = Url::parse(&format!("{}/collections", self.base_url))?;
372
373        // 添加分页参数
374        if let Some(page) = params.page {
375            url.query_pairs_mut().append_pair("page", &page.to_string());
376        }
377
378        if let Some(per_page) = params.per_page {
379            url.query_pairs_mut()
380                .append_pair("per_page", &per_page.to_string());
381        }
382
383        let response = self.send_request(url).await?;
384
385        match response.status() {
386            StatusCode::OK => {
387                let collections_page: CollectionsPage = response.json().await?;
388                Ok(collections_page)
389            }
390            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
391            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
392            status => Err(PexelsError::ApiError(format!(
393                "Get collections failed with status: {status}"
394            ))),
395        }
396    }
397
398    /// 获取收藏中的媒体项目(照片和视频)
399    ///
400    /// # 参数
401    ///
402    /// * `id` - 收藏 ID
403    /// * `params` - 分页参数
404    ///
405    /// # 返回
406    ///
407    /// 包含媒体响应或错误的结果
408    pub async fn get_collection_media(
409        &self,
410        id: &str,
411        params: &PaginationParams,
412    ) -> Result<MediaPage, PexelsError> {
413        let mut url = Url::parse(&format!("{}/collections/{}", self.base_url, id))?;
414
415        // 添加分页参数
416        if let Some(page) = params.page {
417            url.query_pairs_mut().append_pair("page", &page.to_string());
418        }
419
420        if let Some(per_page) = params.per_page {
421            url.query_pairs_mut()
422                .append_pair("per_page", &per_page.to_string());
423        }
424
425        let response = self.send_request(url).await?;
426
427        match response.status() {
428            StatusCode::OK => {
429                let media_page: MediaPage = response.json().await?;
430                Ok(media_page)
431            }
432            StatusCode::NOT_FOUND => Err(PexelsError::NotFound(format!(
433                "Collection with ID {id} not found"
434            ))),
435            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
436            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
437            status => Err(PexelsError::ApiError(format!(
438                "Get collection media failed with status: {status}"
439            ))),
440        }
441    }
442
443    /// 辅助方法,用于向 Pexels API 发送认证请求
444    ///
445    /// # 参数
446    ///
447    /// * `url` - 要发送请求的完整构造 URL
448    ///
449    /// # 返回
450    ///
451    /// 包含 HTTP 响应或错误的结果
452    async fn send_request(&self, url: Url) -> Result<reqwest::Response, PexelsError> {
453        let response = self
454            .client
455            .get(url)
456            .header(header::AUTHORIZATION, &self.api_key)
457            .send()
458            .await?;
459
460        Ok(response)
461    }
462}