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 return ct.starts_with("image/");
137 }
138 return status.is_success();
140 }
141 false
142 }
143 Err(e) => {
144 log::debug!("URL validation failed for {}: {}", url, e);
145 false
146 }
147 }
148}
149
150async fn validate_image_url_get(client: &reqwest::Client, url: &str) -> bool {
152 if url.is_empty() {
153 return false;
154 }
155
156 match client
158 .get(url)
159 .header("Range", "bytes=0-1023")
160 .timeout(Duration::from_secs(5))
161 .send()
162 .await
163 {
164 Ok(resp) => {
165 let status = resp.status();
166 status.is_success() || status.as_u16() == 206 }
168 Err(_) => false,
169 }
170}
171
172pub async fn search_unsplash(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
174 let key = get_unsplash_key().ok_or_else(|| {
175 anyhow::anyhow!("UNSPLASH_ACCESS_KEY not set in environment")
176 })?;
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.results.into_iter().map(|photo| ImageResult {
201 id: photo.id,
202 url: photo.urls.regular,
203 full_url: photo.urls.full,
204 thumb_url: photo.urls.thumb,
205 description: photo.description.or(photo.alt_description).unwrap_or_else(|| "无描述".to_string()),
206 photographer: photo.user.name,
207 photographer_url: photo.user.links.html,
208 width: photo.width,
209 height: photo.height,
210 platform: "Unsplash".to_string(),
211 validated: false, }).collect())
213}
214
215pub async fn search_pexels(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
217 let key = get_pexels_key().ok_or_else(|| {
218 anyhow::anyhow!("PEXELS_API_KEY not set in environment")
219 })?;
220
221 let client = reqwest::Client::builder()
222 .timeout(Duration::from_secs(10))
223 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
224 .build()?;
225
226 let url = format!(
227 "https://api.pexels.com/v1/search?query={}&per_page={}&page=1&locale=zh-CN",
228 query, per_page
229 );
230 let response = client
231 .get(&url)
232 .header("Authorization", key)
233 .send()
234 .await?;
235
236 if !response.status().is_success() {
237 log::warn!("Pexels API error: {}", response.status());
238 return Ok(vec![]);
239 }
240
241 let data: PexelsResponse = response.json().await?;
242
243 Ok(data.photos.into_iter().map(|photo| ImageResult {
244 id: photo.id.to_string(),
245 url: photo.src.large,
246 full_url: photo.src.original,
247 thumb_url: photo.src.medium,
248 description: photo.alt.unwrap_or_else(|| "无描述".to_string()),
249 photographer: photo.photographer,
250 photographer_url: photo.photographer_url,
251 width: photo.width,
252 height: photo.height,
253 platform: "Pexels".to_string(),
254 validated: false,
255 }).collect())
256}
257
258pub async fn search_pixabay(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
260 let key = get_pixabay_key().ok_or_else(|| {
261 anyhow::anyhow!("PIXABAY_API_KEY not set in environment")
262 })?;
263
264 let client = reqwest::Client::builder()
265 .timeout(Duration::from_secs(10))
266 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
267 .build()?;
268
269 let url = format!(
270 "https://pixabay.com/api/?key={}&q={}&per_page={}&page=1&image_type=photo&safesearch=true",
271 key, query, per_page
272 );
273 let response = client
274 .get(&url)
275 .send()
276 .await?;
277
278 if !response.status().is_success() {
279 log::warn!("Pixabay API error: {}", response.status());
280 return Ok(vec![]);
281 }
282
283 let data: PixabayResponse = response.json().await?;
284
285 Ok(data.hits.into_iter().map(|hit| {
286 let user = hit.user.clone();
287 ImageResult {
288 id: hit.id.to_string(),
289 url: hit.webformat_url,
290 full_url: hit.large_image_url,
291 thumb_url: hit.preview_url,
292 description: hit.tags.unwrap_or_else(|| "无描述".to_string()),
293 photographer: user.clone(),
294 photographer_url: format!("https://pixabay.com/users/{}/{}", user, hit.user_id),
295 width: hit.webformat_width,
296 height: hit.webformat_height,
297 platform: "Pixabay".to_string(),
298 validated: false,
299 }
300 }).collect())
301}
302
303async fn validate_images(images: Vec<ImageResult>) -> Vec<ImageResult> {
306 if images.is_empty() {
307 return images;
308 }
309
310 let client = reqwest::Client::builder()
311 .timeout(Duration::from_secs(5))
312 .user_agent("Mozilla/5.0 (compatible; MatrixCode/1.0)")
313 .danger_accept_invalid_certs(true) .build()
315 .unwrap_or_else(|_| reqwest::Client::new());
316
317 log::info!("Validating {} image URLs...", images.len());
318
319 let mut tasks = Vec::new();
321 for img in images {
322 let client = client.clone();
323 let url = img.url.clone();
324
325 let task = tokio::spawn(async move {
326 let valid = validate_image_url(&client, &url).await;
328
329 let valid = if !valid {
331 log::debug!("HEAD failed for {}, trying GET...", url);
332 validate_image_url_get(&client, &url).await
333 } else {
334 true
335 };
336
337 if valid {
338 log::debug!("URL validated: {}", url);
339 } else {
340 log::debug!("URL invalid: {}", url);
341 }
342
343 (img, valid)
344 });
345
346 tasks.push(task);
347 }
348
349 let results = futures_util::future::join_all(tasks).await;
351
352 let mut validated = Vec::new();
354 let total_count = results.len();
355
356 for result in results {
357 if let Ok((mut img, valid)) = result
358 && valid {
359 img.validated = true;
360 validated.push(img);
361 }
362 }
363
364 log::info!("Validated {}/{} images", validated.len(), total_count);
365 validated
366}
367
368pub async fn search_all(query: &str, per_page: u32) -> Result<Vec<ImageResult>> {
372 let mut all_results = Vec::new();
373 let mut errors = Vec::new();
374
375 if get_unsplash_key().is_some() {
377 match search_unsplash(query, per_page).await {
378 Ok(results) => all_results.extend(results),
379 Err(e) => errors.push(format!("Unsplash: {}", e)),
380 }
381 }
382
383 if get_pexels_key().is_some() {
384 match search_pexels(query, per_page).await {
385 Ok(results) => all_results.extend(results),
386 Err(e) => errors.push(format!("Pexels: {}", e)),
387 }
388 }
389
390 if get_pixabay_key().is_some() {
391 match search_pixabay(query, per_page).await {
392 Ok(results) => all_results.extend(results),
393 Err(e) => errors.push(format!("Pixabay: {}", e)),
394 }
395 }
396
397 if all_results.is_empty() && errors.is_empty() {
399 return Err(anyhow::anyhow!(
400 "No image search API keys configured. Set UNSPLASH_ACCESS_KEY, PEXELS_API_KEY, or PIXABAY_API_KEY in environment."
401 ));
402 }
403
404 if !errors.is_empty() && all_results.is_empty() {
406 return Err(anyhow::anyhow!("All searches failed: {}", errors.join("; ")));
407 }
408
409 for e in &errors {
410 log::warn!("Image search partial error: {}", e);
411 }
412
413 let total_count = all_results.len();
415 let validated_results = validate_images(all_results).await;
416
417 if validated_results.is_empty() && !errors.is_empty() {
419 return Err(anyhow::anyhow!(
420 "All {} images failed URL validation. Errors: {}",
421 total_count,
422 errors.join("; ")
423 ));
424 }
425
426 Ok(validated_results)
427}