- Add MetadataSearchRequest, SearchResponse, SearchAssetResult, SearchAlbumResult, SearchFacet, SearchFacetCount models - Create SearchApi with SearchMetadataBuilder supporting 35+ filters - Support filtering by location, dates, camera info, favorites, tags, people, albums, text search, and more - Integrate into Client with client.search().metadata() API
258 lines
7.0 KiB
Rust
258 lines
7.0 KiB
Rust
//! Client for interacting with the Immich API
|
|
|
|
use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi};
|
|
use crate::error::{ImmichError, Result};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
/// Configuration for the Immich client
|
|
#[derive(Debug, Clone)]
|
|
pub struct Config {
|
|
/// Base URL of the Immich server
|
|
pub base_url: String,
|
|
/// API key for authentication
|
|
pub api_key: Option<String>,
|
|
/// Request timeout
|
|
pub timeout: Duration,
|
|
/// User agent string
|
|
pub user_agent: String,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_url: String::new(),
|
|
api_key: None,
|
|
timeout: Duration::from_secs(30),
|
|
user_agent: format!("immich-sdk/{} (Rust)", env!("CARGO_PKG_VERSION")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
/// Create a new config with the given base URL
|
|
pub fn new(base_url: impl Into<String>) -> Self {
|
|
Self {
|
|
base_url: base_url.into(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Set the API key
|
|
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
|
|
self.api_key = Some(api_key.into());
|
|
self
|
|
}
|
|
|
|
/// Set the timeout
|
|
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
|
self.timeout = timeout;
|
|
self
|
|
}
|
|
|
|
/// Set a custom user agent
|
|
pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
|
|
self.user_agent = user_agent.into();
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Internal client data wrapped in Arc for cheap cloning
|
|
#[derive(Debug)]
|
|
struct ClientInner {
|
|
config: Config,
|
|
http: reqwest::Client,
|
|
}
|
|
|
|
/// Client for making requests to the Immich API
|
|
///
|
|
/// This struct is cheap to clone - it uses an internal Arc to share the underlying
|
|
/// HTTP client and configuration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Client {
|
|
inner: Arc<ClientInner>,
|
|
}
|
|
|
|
impl Client {
|
|
/// Create a new client with the given configuration
|
|
pub fn new(config: Config) -> Result<Self> {
|
|
// Validate base URL
|
|
let base_url = if config.base_url.ends_with('/') {
|
|
config.base_url.trim_end_matches('/').to_string()
|
|
} else {
|
|
config.base_url.clone()
|
|
};
|
|
|
|
if base_url.is_empty() {
|
|
return Err(ImmichError::Config("Base URL cannot be empty".to_string()));
|
|
}
|
|
|
|
// Parse to validate URL
|
|
let _ = url::Url::parse(&base_url)?;
|
|
|
|
let http = reqwest::Client::builder()
|
|
.timeout(config.timeout)
|
|
.user_agent(&config.user_agent)
|
|
.build()?;
|
|
|
|
Ok(Self {
|
|
inner: Arc::new(ClientInner {
|
|
config: Config { base_url, ..config },
|
|
http,
|
|
}),
|
|
})
|
|
}
|
|
|
|
/// Create a client from a base URL string
|
|
pub fn from_url(base_url: impl Into<String>) -> Result<Self> {
|
|
Self::new(Config::new(base_url))
|
|
}
|
|
|
|
/// Set the API key
|
|
pub fn with_api_key(self, api_key: impl Into<String>) -> Self {
|
|
// We need to create a new ClientInner since we can't modify Arc contents
|
|
let mut config = self.inner.config.clone();
|
|
config.api_key = Some(api_key.into());
|
|
|
|
Self {
|
|
inner: Arc::new(ClientInner {
|
|
config,
|
|
http: self.inner.http.clone(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Get the configuration
|
|
pub fn config(&self) -> &Config {
|
|
&self.inner.config
|
|
}
|
|
|
|
/// Get the base URL
|
|
pub fn base_url(&self) -> &str {
|
|
&self.inner.config.base_url
|
|
}
|
|
|
|
/// Check if the client has an API key configured
|
|
pub fn has_api_key(&self) -> bool {
|
|
self.inner.config.api_key.is_some()
|
|
}
|
|
|
|
/// Get the underlying HTTP client
|
|
pub fn http(&self) -> &reqwest::Client {
|
|
&self.inner.http
|
|
}
|
|
|
|
/// Create an authenticated request builder
|
|
pub fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
|
|
let url = format!("{}/api{}", self.inner.config.base_url, path);
|
|
let mut builder = self.inner.http.request(method, &url);
|
|
|
|
// Add API key authentication
|
|
if let Some(ref api_key) = self.inner.config.api_key {
|
|
builder = builder.header("x-api-key", api_key);
|
|
}
|
|
|
|
builder
|
|
}
|
|
|
|
/// Execute a request and handle common error cases
|
|
pub async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response> {
|
|
let response = self.inner.http.execute(request).await?;
|
|
|
|
if response.status().is_success() {
|
|
Ok(response)
|
|
} else {
|
|
Err(ImmichError::from_response(response).await)
|
|
}
|
|
}
|
|
|
|
/// Make a GET request
|
|
pub fn get(&self, path: &str) -> reqwest::RequestBuilder {
|
|
self.request(reqwest::Method::GET, path)
|
|
}
|
|
|
|
/// Make a POST request
|
|
pub fn post(&self, path: &str) -> reqwest::RequestBuilder {
|
|
self.request(reqwest::Method::POST, path)
|
|
}
|
|
|
|
/// Make a PUT request
|
|
pub fn put(&self, path: &str) -> reqwest::RequestBuilder {
|
|
self.request(reqwest::Method::PUT, path)
|
|
}
|
|
|
|
/// Make a DELETE request
|
|
pub fn delete(&self, path: &str) -> reqwest::RequestBuilder {
|
|
self.request(reqwest::Method::DELETE, path)
|
|
}
|
|
|
|
/// Make a PATCH request
|
|
pub fn patch(&self, path: &str) -> reqwest::RequestBuilder {
|
|
self.request(reqwest::Method::PATCH, path)
|
|
}
|
|
|
|
/// Access the albums API
|
|
pub fn albums(&self) -> AlbumsApi {
|
|
AlbumsApi::new(self.clone())
|
|
}
|
|
|
|
/// Access the assets API
|
|
pub fn assets(&self) -> AssetsApi {
|
|
AssetsApi::new(self.clone())
|
|
}
|
|
|
|
/// Access the search API
|
|
pub fn search(&self) -> SearchApi {
|
|
SearchApi::new(self.clone())
|
|
}
|
|
|
|
/// Access the server API
|
|
pub fn server(&self) -> ServerApi {
|
|
ServerApi::new(self.clone())
|
|
}
|
|
|
|
/// Access the timeline API
|
|
pub fn timeline(&self) -> TimelineApi {
|
|
TimelineApi::new(self.clone())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_config_builder() {
|
|
let config = Config::new("https://example.com")
|
|
.with_api_key("test-key")
|
|
.with_timeout(Duration::from_secs(60));
|
|
|
|
assert_eq!(config.base_url, "https://example.com");
|
|
assert_eq!(config.api_key, Some("test-key".to_string()));
|
|
assert_eq!(config.timeout, Duration::from_secs(60));
|
|
}
|
|
|
|
#[test]
|
|
fn test_client_creation() {
|
|
let client = Client::from_url("https://example.com").unwrap();
|
|
assert_eq!(client.base_url(), "https://example.com");
|
|
assert!(!client.has_api_key());
|
|
|
|
let config = Config::new("https://example.com").with_api_key("test-key");
|
|
let client = Client::new(config).unwrap();
|
|
assert!(client.has_api_key());
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_normalization() {
|
|
let client = Client::from_url("https://example.com/").unwrap();
|
|
assert_eq!(client.base_url(), "https://example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_url_error() {
|
|
let result = Client::from_url("");
|
|
assert!(result.is_err());
|
|
}
|
|
}
|