use reqwest::multipart::{Form, Part};
use reqwest::Client;
use serde::Deserialize;
use std::path::Path;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum BilberryError {
#[error("Request failed: {0}")]
RequestError(#[from] reqwest::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("API Error: {0}")]
ApiError(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("JSON parsing error: {0}")]
JsonError(String),
#[error("{0}")]
Custom(String),
}
pub type Result<T> = std::result::Result<T, BilberryError>;
#[derive(Debug, Clone)]
pub struct BilberryConfig {
pub api_key: String,
pub api_id: String,
pub base_url: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SearchResult {
pub id: u64, pub file_name: String,
pub file_type: String,
pub content_type: String,
pub similarity: Option<f64>,
pub similarity_score: Option<f64>,
pub embedding_size: Option<u32>,
pub file_size: u64,
pub created_at: String,
}
impl SearchResult {
pub fn get_filename(&self) -> String {
self.file_name.clone()
}
pub fn get_similarity_score(&self) -> f64 {
self.similarity_score
.or(self.similarity)
.unwrap_or(0.0)
}
}
#[derive(Debug, Deserialize)]
pub struct ItemResponse {
pub id: u64, pub file_name: String, pub file_type: String,
pub content_type: String,
pub created_at: String,
pub file_size: u64,
pub embedding_size: Option<u32>, }
pub struct BilberryVector {
client: Client,
config: BilberryConfig,
}
impl BilberryVector {
fn new(config: BilberryConfig) -> Self {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self { client, config }
}
pub async fn search_by_path(&self, image_path: &str, top_k: Option<u32>) -> Result<Vec<SearchResult>> {
self.search_by_path_with_options(image_path, &SearchOptions {
top_k,
content_type: None,
}).await
}
pub async fn search_by_bytes(&self, image_data: &[u8], top_k: Option<u32>) -> Result<Vec<SearchResult>> {
self.search_by_bytes_with_options(image_data, &SearchOptions {
top_k,
content_type: None,
}).await
}
pub async fn search_by_path_with_options(&self, image_path: &str, options: &SearchOptions) -> Result<Vec<SearchResult>> {
let path = Path::new(image_path);
if !path.exists() {
return Err(BilberryError::FileNotFound(image_path.to_string()));
}
let file_data = std::fs::read(image_path)?;
let filename = path.file_name()
.and_then(|f| f.to_str())
.unwrap_or("image.jpg");
self.search_internal(&file_data, filename, options).await
}
pub async fn search_by_bytes_with_options(&self, image_data: &[u8], options: &SearchOptions) -> Result<Vec<SearchResult>> {
self.search_internal(image_data, "image.jpg", options).await
}
async fn search_internal(&self, image_data: &[u8], filename: &str, options: &SearchOptions) -> Result<Vec<SearchResult>> {
let base_url = self.config.base_url.as_deref().unwrap_or("https://appbilberry.com");
let url = format!("{}/search/imagez", base_url);
let part = Part::bytes(image_data.to_vec())
.file_name(filename.to_string())
.mime_str("image/jpeg")
.map_err(|e| BilberryError::Custom(format!("Failed to create multipart: {}", e)))?;
let form = Form::new().part("file", part);
let top_k_string = options.top_k.unwrap_or(5).to_string();
let mut query_params = vec![
("user_email", self.config.api_id.as_str()),
("api_key", self.config.api_key.as_str()),
("top_k", top_k_string.as_str()),
];
let content_type_string;
if let Some(content_type) = &options.content_type {
content_type_string = content_type.clone();
query_params.push(("content_type", &content_type_string));
}
let response = self.client
.post(&url)
.multipart(form)
.query(&query_params)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(BilberryError::ApiError(format!("HTTP {}: {}", status, text)));
}
let response_text = response.text().await?;
let results: Vec<SearchResult> = serde_json::from_str(&response_text)
.map_err(|e| BilberryError::JsonError(format!("Failed to parse JSON response: {}. Response was: {}", e, response_text)))?;
Ok(results)
}
pub async fn get_all_items(&self, content_type: Option<&str>) -> Result<Vec<ItemResponse>> {
let base_url = self.config.base_url.as_deref().unwrap_or("https://appbilberry.com");
let url = format!("{}/itemz", base_url);
let mut query_params = vec![
("user_email", self.config.api_id.as_str()),
("api_key", self.config.api_key.as_str()),
];
let content_type_string;
if let Some(content_type) = content_type {
content_type_string = content_type.to_string();
query_params.push(("content_type", &content_type_string));
}
let response = self.client
.get(&url)
.query(&query_params)
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(BilberryError::ApiError(format!("HTTP {}: {}", status, text)));
}
let response_text = response.text().await?;
println!("Items API response: {}", response_text);
let items: Vec<ItemResponse> = serde_json::from_str(&response_text)
.map_err(|e| BilberryError::JsonError(format!("Failed to parse items JSON: {}. Response was: {}", e, response_text)))?;
Ok(items)
}
pub async fn download_file(&self, item_id: &str) -> Result<Vec<u8>> {
let base_url = self.config.base_url.as_deref().unwrap_or("https://appbilberry.com");
let url = format!("{}/itemz/{}/download", base_url, item_id);
let response = self.client
.get(&url)
.query(&[
("user_email", self.config.api_id.as_str()),
("api_key", self.config.api_key.as_str()),
])
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
return Err(BilberryError::ApiError(format!("HTTP {}: {}", status, text)));
}
let bytes = response.bytes().await?;
Ok(bytes.to_vec())
}
}
pub struct SearchOptions {
pub top_k: Option<u32>,
pub content_type: Option<String>,
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
top_k: Some(5),
content_type: None,
}
}
}
pub struct BilberryClient {
config: BilberryConfig,
}
impl BilberryClient {
pub fn get_vec(&self) -> BilberryVector {
BilberryVector::new(self.config.clone())
}
pub fn get_vector(&self) -> BilberryVector {
self.get_vec()
}
}
pub fn init(config: BilberryConfig) -> Result<BilberryClient> {
if config.api_key.is_empty() {
return Err(BilberryError::Custom("api_key is required".to_string()));
}
if config.api_id.is_empty() {
return Err(BilberryError::Custom("api_id is required".to_string()));
}
Ok(BilberryClient { config })
}