use crate::error::{PixabayError, Result};
use crate::models::*;
use reqwest::Client;
use url::Url;
const BASE_URL: &str = "https://pixabay.com/api/";
const VIDEO_BASE_URL: &str = "https://pixabay.com/api/videos/";
#[derive(Debug, Clone)]
pub struct Pixabay {
pub api_key: String,
client: Client,
}
impl Pixabay {
pub fn new(api_key: String) -> Self {
Self {
api_key,
client: Client::new(),
}
}
pub async fn search_images(
&self,
query: &str,
per_page: Option<u32>,
page: Option<u32>,
) -> Result<ImageResponse> {
let per_page = per_page.unwrap_or(20).clamp(3, 200);
let page = page.unwrap_or(1);
let mut url = Url::parse(BASE_URL)?;
url.query_pairs_mut()
.append_pair("key", &self.api_key)
.append_pair("q", query)
.append_pair("per_page", &per_page.to_string())
.append_pair("page", &page.to_string());
let response = self.client.get(url).send().await?;
self.handle_response(response).await
}
async fn handle_response<T: serde::de::DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T> {
let status = response.status();
if status.is_success() {
Ok(response.json().await?)
} else if status.as_u16() == 429 {
Err(PixabayError::RateLimitExceeded)
} else if status.as_u16() == 400 {
let error_text = response.text().await?;
Err(PixabayError::ApiError(format!("错误请求: {}", error_text)))
} else if status.as_u16() == 401 || status.as_u16() == 403 {
Err(PixabayError::InvalidApiKey)
} else {
let error_text = response.text().await?;
Err(PixabayError::ApiError(format!(
"HTTP {}: {}",
status.as_u16(),
error_text
)))
}
}
pub async fn search_images_advanced(&self, params: SearchImageParams) -> Result<ImageResponse> {
let per_page = params.per_page.unwrap_or(20).clamp(3, 200);
let mut url = Url::parse(BASE_URL)?;
let mut query = url.query_pairs_mut();
query.append_pair("key", &self.api_key);
if let Some(q) = ¶ms.query {
if q.len() > 100 {
drop(query);
return Err(PixabayError::ApiError(
"查询字符串不能超过 100 个字符".to_string(),
));
}
query.append_pair("q", q);
}
query.append_pair("per_page", &per_page.to_string());
query.append_pair("page", ¶ms.page.unwrap_or(1).to_string());
if let Some(image_type) = ¶ms.image_type {
query.append_pair("image_type", &image_type.to_string());
}
if let Some(orientation) = ¶ms.orientation {
query.append_pair("orientation", &orientation.to_string());
}
if let Some(category) = ¶ms.category {
query.append_pair("category", &category.to_string());
}
if let Some(min_width) = params.min_width {
query.append_pair("min_width", &min_width.to_string());
}
if let Some(min_height) = params.min_height {
query.append_pair("min_height", &min_height.to_string());
}
if let Some(colors) = ¶ms.colors {
query.append_pair("colors", colors);
}
if let Some(editors_choice) = params.editors_choice {
query.append_pair("editors_choice", &editors_choice.to_string());
}
if let Some(safesearch) = params.safesearch {
query.append_pair("safesearch", &safesearch.to_string());
}
if let Some(order) = ¶ms.order {
query.append_pair("order", &order.to_string());
}
if let Some(lang) = ¶ms.lang {
query.append_pair("lang", &lang.to_string());
}
drop(query);
let response = self.client.get(url).send().await?;
self.handle_response(response).await
}
pub async fn get_image(&self, id: u64) -> Result<Image> {
let mut url = Url::parse(BASE_URL)?;
url.query_pairs_mut()
.append_pair("key", &self.api_key)
.append_pair("id", &id.to_string());
let response = self.client.get(url).send().await?;
let image_response: ImageResponse = self.handle_response(response).await?;
image_response
.hits
.into_iter()
.next()
.ok_or_else(|| PixabayError::ApiError(format!("未找到 ID 为 {} 的图片", id)))
}
pub async fn search_videos(
&self,
query: &str,
per_page: Option<u32>,
page: Option<u32>,
) -> Result<VideoResponse> {
let per_page = per_page.unwrap_or(20).clamp(3, 200);
let page = page.unwrap_or(1);
let mut url = Url::parse(VIDEO_BASE_URL)?;
url.query_pairs_mut()
.append_pair("key", &self.api_key)
.append_pair("q", query)
.append_pair("per_page", &per_page.to_string())
.append_pair("page", &page.to_string());
let response = self.client.get(url).send().await?;
self.handle_response(response).await
}
pub async fn search_videos_advanced(&self, params: SearchVideoParams) -> Result<VideoResponse> {
let per_page = params.per_page.unwrap_or(20).clamp(3, 200);
let mut url = Url::parse(VIDEO_BASE_URL)?;
let mut query = url.query_pairs_mut();
query.append_pair("key", &self.api_key);
if let Some(q) = ¶ms.query {
if q.len() > 100 {
drop(query);
return Err(PixabayError::ApiError(
"查询字符串不能超过 100 个字符".to_string(),
));
}
query.append_pair("q", q);
}
query.append_pair("per_page", &per_page.to_string());
query.append_pair("page", ¶ms.page.unwrap_or(1).to_string());
if let Some(video_type) = ¶ms.video_type {
query.append_pair("video_type", &video_type.to_string());
}
if let Some(category) = ¶ms.category {
query.append_pair("category", &category.to_string());
}
if let Some(min_width) = params.min_width {
query.append_pair("min_width", &min_width.to_string());
}
if let Some(min_height) = params.min_height {
query.append_pair("min_height", &min_height.to_string());
}
if let Some(editors_choice) = params.editors_choice {
query.append_pair("editors_choice", &editors_choice.to_string());
}
if let Some(safesearch) = params.safesearch {
query.append_pair("safesearch", &safesearch.to_string());
}
if let Some(order) = ¶ms.order {
query.append_pair("order", &order.to_string());
}
if let Some(lang) = ¶ms.lang {
query.append_pair("lang", &lang.to_string());
}
drop(query);
let response = self.client.get(url).send().await?;
self.handle_response(response).await
}
pub async fn get_video(&self, id: u64) -> Result<Video> {
let mut url = Url::parse(VIDEO_BASE_URL)?;
url.query_pairs_mut()
.append_pair("key", &self.api_key)
.append_pair("id", &id.to_string());
let response = self.client.get(url).send().await?;
let video_response: VideoResponse = self.handle_response(response).await?;
video_response
.hits
.into_iter()
.next()
.ok_or_else(|| PixabayError::ApiError(format!("未找到 ID 为 {} 的视频", id)))
}
}
#[derive(Debug, Clone, Default)]
pub struct SearchImageParams {
pub query: Option<String>,
pub per_page: Option<u32>,
pub page: Option<u32>,
pub image_type: Option<ImageType>,
pub orientation: Option<Orientation>,
pub category: Option<Category>,
pub min_width: Option<u32>,
pub min_height: Option<u32>,
pub colors: Option<String>,
pub editors_choice: Option<bool>,
pub safesearch: Option<bool>,
pub order: Option<Order>,
pub lang: Option<Language>,
}
impl SearchImageParams {
pub fn new() -> Self {
Self::default()
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
pub fn page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
pub fn image_type(mut self, image_type: ImageType) -> Self {
self.image_type = Some(image_type);
self
}
pub fn orientation(mut self, orientation: Orientation) -> Self {
self.orientation = Some(orientation);
self
}
pub fn category(mut self, category: Category) -> Self {
self.category = Some(category);
self
}
pub fn min_width(mut self, min_width: u32) -> Self {
self.min_width = Some(min_width);
self
}
pub fn min_height(mut self, min_height: u32) -> Self {
self.min_height = Some(min_height);
self
}
pub fn colors(mut self, colors: impl Into<String>) -> Self {
self.colors = Some(colors.into());
self
}
pub fn editors_choice(mut self, editors_choice: bool) -> Self {
self.editors_choice = Some(editors_choice);
self
}
pub fn safesearch(mut self, safesearch: bool) -> Self {
self.safesearch = Some(safesearch);
self
}
pub fn order(mut self, order: Order) -> Self {
self.order = Some(order);
self
}
pub fn lang(mut self, lang: Language) -> Self {
self.lang = Some(lang);
self
}
}
#[derive(Debug, Clone, Default)]
pub struct SearchVideoParams {
pub query: Option<String>,
pub per_page: Option<u32>,
pub page: Option<u32>,
pub video_type: Option<VideoType>,
pub category: Option<Category>,
pub min_width: Option<u32>,
pub min_height: Option<u32>,
pub editors_choice: Option<bool>,
pub safesearch: Option<bool>,
pub order: Option<Order>,
pub lang: Option<Language>,
}
impl SearchVideoParams {
pub fn new() -> Self {
Self::default()
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
pub fn page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
pub fn video_type(mut self, video_type: VideoType) -> Self {
self.video_type = Some(video_type);
self
}
pub fn category(mut self, category: Category) -> Self {
self.category = Some(category);
self
}
pub fn min_width(mut self, min_width: u32) -> Self {
self.min_width = Some(min_width);
self
}
pub fn min_height(mut self, min_height: u32) -> Self {
self.min_height = Some(min_height);
self
}
pub fn editors_choice(mut self, editors_choice: bool) -> Self {
self.editors_choice = Some(editors_choice);
self
}
pub fn safesearch(mut self, safesearch: bool) -> Self {
self.safesearch = Some(safesearch);
self
}
pub fn order(mut self, order: Order) -> Self {
self.order = Some(order);
self
}
pub fn lang(mut self, lang: Language) -> Self {
self.lang = Some(lang);
self
}
}