fix: adaptation structs Imago API v2 - ExifCamera/ExifGps/AiData/OcrData
- Corrige ExifData pour matcher l'API réelle (camera/gps/raw nestés) - Corrige OcrData (has_text ajouté) - Corrige AiData (confidence, model_used, processed_at, prompt/output_tokens) - Supprime les champs obsolètes (category, url_summary, window_title) - get_image expose maintenant les champs EXIF/GPS complets - Plus d'erreur 'missing field tags' sur le list/detail
This commit is contained in:
+180
-35
@@ -1,36 +1,139 @@
|
||||
//! Client HTTP pour l'API Imago
|
||||
//! Client HTTP pour l'API Imago v2
|
||||
//!
|
||||
//! API base: /api/v1/images
|
||||
//! Auth: X-API-Key header
|
||||
|
||||
use anyhow::Result;
|
||||
use reqwest::{Client, multipart};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
// ─── Response types ────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UploadResponse {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub id: i64,
|
||||
pub uuid: String,
|
||||
pub original_name: String,
|
||||
pub status: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Vue allégée pour les listes (ImageSummary)
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ImageSummary {
|
||||
pub id: i64,
|
||||
pub uuid: String,
|
||||
pub original_name: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub file_size: Option<i64>,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
pub uploaded_at: Option<String>,
|
||||
pub processing_status: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub client_name: Option<String>,
|
||||
pub ai_tags: Option<Vec<String>>,
|
||||
pub ai_description: Option<String>,
|
||||
pub thumbnail_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Vue détaillée (ImageDetail)
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ImageDetail {
|
||||
// ImageBase
|
||||
pub id: i64,
|
||||
pub uuid: String,
|
||||
pub original_name: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub file_size: Option<i64>,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
pub uploaded_at: Option<String>,
|
||||
pub processing_status: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub client_name: Option<String>,
|
||||
// Nested objects
|
||||
pub exif: Option<ExifData>,
|
||||
pub ocr: Option<OcrData>,
|
||||
pub ai: Option<AiData>,
|
||||
pub processing: Option<ProcessingInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ImageInfo {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub tags: Vec<String>,
|
||||
pub struct ExifCamera {
|
||||
pub make: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub lens: Option<String>,
|
||||
pub iso: Option<i32>,
|
||||
pub aperture: Option<f64>,
|
||||
pub shutter_speed: Option<String>,
|
||||
pub focal_length: Option<String>,
|
||||
pub flash: Option<bool>,
|
||||
pub orientation: Option<String>,
|
||||
pub software: Option<String>,
|
||||
pub taken_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ExifGps {
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub altitude: Option<f64>,
|
||||
pub has_gps: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ExifData {
|
||||
pub camera: Option<ExifCamera>,
|
||||
pub gps: Option<ExifGps>,
|
||||
#[serde(default)]
|
||||
pub raw: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct OcrData {
|
||||
pub text: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub confidence: Option<f64>,
|
||||
pub has_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AiData {
|
||||
pub description: Option<String>,
|
||||
pub ocr_text: Option<String>,
|
||||
pub created_at: String,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub size_bytes: Option<u64>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub confidence: Option<f64>,
|
||||
pub model_used: Option<String>,
|
||||
pub processed_at: Option<String>,
|
||||
pub prompt_tokens: Option<i64>,
|
||||
pub output_tokens: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
pub results: Vec<ImageInfo>,
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ProcessingInfo {
|
||||
pub status: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub started_at: Option<String>,
|
||||
pub done_at: Option<String>,
|
||||
}
|
||||
|
||||
/// Réponse paginée
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PaginatedImages {
|
||||
pub total: usize,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
pub pages: usize,
|
||||
pub items: Vec<ImageSummary>,
|
||||
pub storage_used_mb: Option<f64>,
|
||||
pub storage_quota_mb: Option<i64>,
|
||||
pub quota_pct: Option<f64>,
|
||||
}
|
||||
|
||||
/// Client pour l'API Imago
|
||||
// ─── Client ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Client pour l'API Imago v2
|
||||
#[derive(Clone)]
|
||||
pub struct ImagoClient {
|
||||
client: Client,
|
||||
pub base_url: String,
|
||||
@@ -38,6 +141,7 @@ pub struct ImagoClient {
|
||||
}
|
||||
|
||||
impl ImagoClient {
|
||||
/// Crée un nouveau client Imago
|
||||
pub fn new(base_url: String, api_key: String) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
@@ -45,6 +149,11 @@ impl ImagoClient {
|
||||
Ok(Self { client, base_url, api_key })
|
||||
}
|
||||
|
||||
/// Construit l'URL de base pour l'API images
|
||||
fn images_base(&self) -> String {
|
||||
format!("{}/api/v1/images", self.base_url.trim_end_matches('/'))
|
||||
}
|
||||
|
||||
/// Upload une image vers Imago
|
||||
pub async fn upload_image(
|
||||
&self,
|
||||
@@ -64,8 +173,8 @@ impl ImagoClient {
|
||||
}
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/images/upload", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.post(format!("{}/upload", self.images_base()))
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -78,16 +187,16 @@ impl ImagoClient {
|
||||
Ok(upload)
|
||||
}
|
||||
|
||||
/// Recherche des images
|
||||
pub async fn search(&self, query: &str, limit: usize) -> Result<SearchResponse> {
|
||||
/// Liste les images (paginated)
|
||||
pub async fn list_images(&self, page: usize, limit: usize) -> Result<PaginatedImages> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/search", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.query(&[("q", query), ("limit", &limit.to_string())])
|
||||
.get(self.images_base())
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.query(&[("page", &page.to_string()), ("page_size", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: SearchResponse = response
|
||||
let result: PaginatedImages = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
@@ -95,27 +204,44 @@ impl ImagoClient {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Récupère les infos d'une image
|
||||
pub async fn get_image(&self, id: &str) -> Result<ImageInfo> {
|
||||
/// Recherche des images
|
||||
pub async fn search(&self, query: &str, limit: usize, page: usize) -> Result<PaginatedImages> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/{}", self.base_url, id))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.get(self.images_base())
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.query(&[("search", query), ("page_size", &limit.to_string()), ("page", &page.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let info: ImageInfo = response
|
||||
let result: PaginatedImages = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(info)
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Récupère des données brutes (image, thumbnail) depuis une URL Imago
|
||||
pub async fn get_raw(&self, url: &str) -> Result<(Vec<u8>, String)> {
|
||||
/// Récupère les détails d'une image
|
||||
pub async fn get_image(&self, id: i64) -> Result<ImageDetail> {
|
||||
let response = self.client
|
||||
.get(url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.get(format!("{}/{}", self.images_base(), id))
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let detail: ImageDetail = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(detail)
|
||||
}
|
||||
|
||||
/// Récupère le thumbnail d'une image (raw bytes)
|
||||
pub async fn get_thumbnail(&self, id: i64) -> Result<(Vec<u8>, String)> {
|
||||
let response = self.client
|
||||
.get(format!("{}/{}/thumbnail", self.images_base(), id))
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
@@ -128,7 +254,26 @@ impl ImagoClient {
|
||||
.to_string();
|
||||
|
||||
let data = response.bytes().await?.to_vec();
|
||||
Ok((data, content_type))
|
||||
}
|
||||
|
||||
/// Récupère le fichier original (raw bytes)
|
||||
pub async fn get_file(&self, id: i64) -> Result<(Vec<u8>, String)> {
|
||||
let response = self.client
|
||||
.get(format!("{}/{}/file", self.images_base(), id))
|
||||
.header("X-API-Key", &self.api_key)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("image/png")
|
||||
.to_string();
|
||||
|
||||
let data = response.bytes().await?.to_vec();
|
||||
Ok((data, content_type))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user