1use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9#[derive(Debug, Deserialize)]
11struct UnsplashResponse {
12 results: Vec<UnsplashPhoto>,
13}
14
15#[derive(Debug, Deserialize)]
16struct UnsplashPhoto {
17 id: String,
18 urls: UnsplashUrls,
19 description: Option<String>,
20 alt_description: Option<String>,
21 user: UnsplashUser,
22 width: u32,
23 height: u32,
24}
25
26#[derive(Debug, Deserialize)]
27struct UnsplashUrls {
28 regular: String,
29 full: String,
30 thumb: String,
31}
32
33#[derive(Debug, Deserialize)]
34struct UnsplashUser {
35 name: String,
36 links: UnsplashLinks,
37}
38
39#[derive(Debug, Deserialize)]
40struct UnsplashLinks {
41 html: String,
42}
43
44#[derive(Debug, Deserialize)]
46struct PexelsResponse {
47 photos: Vec<PexelsPhoto>,
48}
49
50#[derive(Debug, Deserialize)]
51struct PexelsPhoto {
52 id: u32,
53 src: PexelsSrc,
54 alt: Option<String>,
55 photographer: String,
56 photographer_url: String,
57 width: u32,
58 height: u32,
59}
60
61#[derive(Debug, Deserialize)]
62struct PexelsSrc {
63 large: String,
64 original: String,
65 medium: String,
66}
67
68#[derive(Debug, Deserialize)]
70struct PixabayResponse {
71 hits: Vec<PixabayHit>,
72}
73
74#[derive(Debug, Deserialize)]
75struct PixabayHit {
76 id: u32,
77 #[serde(rename = "webformatURL")]
78 webformat_url: String,
79 #[serde(rename = "largeImageURL")]
80 large_image_url: String,
81 #[serde(rename = "previewURL")]
82 preview_url: String,
83 tags: Option<String>,
84 user: String,
85 user_id: u32,
86 #[serde(rename = "webformatWidth")]
87 webformat_width: u32,
88 #[serde(rename = "webformatHeight")]
89 webformat_height: u32,
90}
91
92#[derive(Debug, Serialize, Clone)]
94pub struct ImageResult {
95 pub id: String,
96 pub url: String,
97 pub full_url: String,
98 pub thumb_url: String,
99 pub description: String,
100 pub photographer: String,
101 pub photographer_url: String,
102 pub width: u32,
103 pub height: u32,
104 pub platform: String,
105 pub validated: bool,
107}
108
109fn get_unsplash_key() -> Option<String> {
112 std::env::var("UNSPLASH_ACCESS_KEY").ok()
113}
114
115fn get_pexels_key() -> Option<String> {
116 std::env::var("PEXELS_API_KEY").ok()
117}
118
119fn get_pixabay_key() -> Option<String> {
120 std::env::var("PIXABAY_API_KEY").ok()
121}
122
123async fn validate_image_url(client: &reqwest::Client, url: &str) -> bool {
125 if url.is_empty() {
126 return false;
127 }
128
129 match client.head(url).send().await {
130 Ok(resp) => {
131 let status = resp.status();
132 if status.is_success() || status.as_u16() == 302 {
133 if let Some(content_type) = resp.headers().get("content-type")
135 && let Ok(ct) = content_type.to_str()
136 {
137 return ct.starts_with("image/");
138 }
139 return status.is_success();
141 }
142 false
143 }
144 Err(e) => {
145 log::debug!("URL validation failed for {}: {}", url, e);
146 false
147 }
148 }
149}
150
151async fn validate_image_url_get(client: &reqwest::Client, url: &str) -> bool {
153 if url.is_empty() {
154 return false;
155 }
156
157 match client
159 .get(url)
160 .header("Range", "bytes=0-1023")
161 .timeout(Duration::from_secs(5))
162 .send()
163 .await
164 {
165 Ok(resp) => {
166 let status = resp.status();
167 status.is_success() || status.as_u16() == 206 }
169 Err(_) => false,
170 }
171}
172
173pub async fn search_unsplash(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
175 let key = get_unsplash_key()
176 .ok_or_else(|| anyhow::anyhow!("UNSPLASH_ACCESS_KEY not set in environment"))?;
177
178 let client = reqwest::Client::builder()
179 .timeout(Duration::from_secs(10))
180 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
181 .build()?;
182
183 let url = format!(
184 "https://api.unsplash.com/search/photos?query={}&per_page={}&page=1",
185 query, per_page
186 );
187 let response = client
188 .get(&url)
189 .header("Authorization", format!("Client-ID {}", key))
190 .send()
191 .await?;
192
193 if !response.status().is_success() {
194 log::warn!("Unsplash API error: {}", response.status());
195 return Ok(vec![]);
196 }
197
198 let data: UnsplashResponse = response.json().await?;
199
200 Ok(data
201 .results
202 .into_iter()
203 .map(|photo| ImageResult {
204 id: photo.id,
205 url: photo.urls.regular,
206 full_url: photo.urls.full,
207 thumb_url: photo.urls.thumb,
208 description: photo
209 .description
210 .or(photo.alt_description)
211 .unwrap_or_else(|| "无描述".to_string()),
212 photographer: photo.user.name,
213 photographer_url: photo.user.links.html,
214 width: photo.width,
215 height: photo.height,
216 platform: "Unsplash".to_string(),
217 validated: false, })
219 .collect())
220}
221
222pub async fn search_pexels(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
224 let key =
225 get_pexels_key().ok_or_else(|| anyhow::anyhow!("PEXELS_API_KEY not set in environment"))?;
226
227 let client = reqwest::Client::builder()
228 .timeout(Duration::from_secs(10))
229 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
230 .build()?;
231
232 let url = format!(
233 "https://api.pexels.com/v1/search?query={}&per_page={}&page=1&locale=zh-CN",
234 query, per_page
235 );
236 let response = client.get(&url).header("Authorization", key).send().await?;
237
238 if !response.status().is_success() {
239 log::warn!("Pexels API error: {}", response.status());
240 return Ok(vec![]);
241 }
242
243 let data: PexelsResponse = response.json().await?;
244
245 Ok(data
246 .photos
247 .into_iter()
248 .map(|photo| ImageResult {
249 id: photo.id.to_string(),
250 url: photo.src.large,
251 full_url: photo.src.original,
252 thumb_url: photo.src.medium,
253 description: photo.alt.unwrap_or_else(|| "无描述".to_string()),
254 photographer: photo.photographer,
255 photographer_url: photo.photographer_url,
256 width: photo.width,
257 height: photo.height,
258 platform: "Pexels".to_string(),
259 validated: false,
260 })
261 .collect())
262}
263
264pub async fn search_pixabay(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
266 let key = get_pixabay_key()
267 .ok_or_else(|| anyhow::anyhow!("PIXABAY_API_KEY not set in environment"))?;
268
269 let client = reqwest::Client::builder()
270 .timeout(Duration::from_secs(10))
271 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
272 .build()?;
273
274 let url = format!(
275 "https://pixabay.com/api/?key={}&q={}&per_page={}&page=1&image_type=photo&safesearch=true",
276 key, query, per_page
277 );
278 let response = client.get(&url).send().await?;
279
280 if !response.status().is_success() {
281 log::warn!("Pixabay API error: {}", response.status());
282 return Ok(vec![]);
283 }
284
285 let data: PixabayResponse = response.json().await?;
286
287 Ok(data
288 .hits
289 .into_iter()
290 .map(|hit| {
291 let user = hit.user.clone();
292 ImageResult {
293 id: hit.id.to_string(),
294 url: hit.webformat_url,
295 full_url: hit.large_image_url,
296 thumb_url: hit.preview_url,
297 description: hit.tags.unwrap_or_else(|| "无描述".to_string()),
298 photographer: user.clone(),
299 photographer_url: format!("https://pixabay.com/users/{}/{}", user, hit.user_id),
300 width: hit.webformat_width,
301 height: hit.webformat_height,
302 platform: "Pixabay".to_string(),
303 validated: false,
304 }
305 })
306 .collect())
307}
308
309async fn validate_images(images: Vec<ImageResult>) -> Vec<ImageResult> {
312 if images.is_empty() {
313 return images;
314 }
315
316 let client = reqwest::Client::builder()
317 .timeout(Duration::from_secs(5))
318 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
319 .danger_accept_invalid_certs(true) .build()
321 .unwrap_or_else(|_| reqwest::Client::new());
322
323 log::info!("Validating {} image URLs...", images.len());
324
325 let mut tasks = Vec::new();
327 for img in images {
328 let client = client.clone();
329 let url = img.url.clone();
330
331 let task = tokio::spawn(async move {
332 let valid = validate_image_url(&client, &url).await;
334
335 let valid = if !valid {
337 log::debug!("HEAD failed for {}, trying GET...", url);
338 validate_image_url_get(&client, &url).await
339 } else {
340 true
341 };
342
343 if valid {
344 log::debug!("URL validated: {}", url);
345 } else {
346 log::debug!("URL invalid: {}", url);
347 }
348
349 (img, valid)
350 });
351
352 tasks.push(task);
353 }
354
355 let results = futures_util::future::join_all(tasks).await;
357
358 let mut validated = Vec::new();
360 let total_count = results.len();
361
362 for result in results {
363 if let Ok((mut img, valid)) = result
364 && valid
365 {
366 img.validated = true;
367 validated.push(img);
368 }
369 }
370
371 log::info!("Validated {}/{} images", validated.len(), total_count);
372 validated
373}
374
375pub async fn search_all(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
379 let mut all_results = Vec::new();
380 let mut errors = Vec::new();
381
382 if get_unsplash_key().is_some() {
384 match search_unsplash(query, per_page).await {
385 Ok(results) => all_results.extend(results),
386 Err(e) => errors.push(format!("Unsplash: {}", e)),
387 }
388 }
389
390 if get_pexels_key().is_some() {
391 match search_pexels(query, per_page).await {
392 Ok(results) => all_results.extend(results),
393 Err(e) => errors.push(format!("Pexels: {}", e)),
394 }
395 }
396
397 if get_pixabay_key().is_some() {
398 match search_pixabay(query, per_page).await {
399 Ok(results) => all_results.extend(results),
400 Err(e) => errors.push(format!("Pixabay: {}", e)),
401 }
402 }
403
404 if all_results.is_empty() && errors.is_empty() {
406 return Err(anyhow::anyhow!(
407 "No image search API keys configured. Set UNSPLASH_ACCESS_KEY, PEXELS_API_KEY, or PIXABAY_API_KEY in environment."
408 ));
409 }
410
411 if !errors.is_empty() && all_results.is_empty() {
413 return Err(anyhow::anyhow!(
414 "All searches failed: {}",
415 errors.join("; ")
416 ));
417 }
418
419 for e in &errors {
420 log::warn!("Image search partial error: {}", e);
421 }
422
423 let total_count = all_results.len();
425 let validated_results = validate_images(all_results).await;
426
427 if validated_results.is_empty() && !errors.is_empty() {
429 return Err(anyhow::anyhow!(
430 "All {} images failed URL validation. Errors: {}",
431 total_count,
432 errors.join("; ")
433 ));
434 }
435
436 Ok(validated_results)
437}