Add search/metadata API endpoint

- 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
This commit is contained in:
Joakim Hulthe
2026-04-14 19:35:06 +00:00
parent 3284a18dcb
commit c55d2b9080
4 changed files with 577 additions and 1 deletions

View File

@@ -344,6 +344,180 @@ pub struct TimeBucketAssetResponse {
pub visibility: Vec<AssetVisibility>,
}
/// Metadata search request for filtering assets
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct MetadataSearchRequest {
/// Filter by album IDs
#[serde(skip_serializing_if = "Option::is_none")]
pub album_ids: Option<Vec<AssetId>>,
/// Filter by file checksum
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
/// Filter by city name
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
/// Filter by country name
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
/// Filter by creation date (after)
#[serde(skip_serializing_if = "Option::is_none")]
pub created_after: Option<DateTime<Utc>>,
/// Filter by creation date (before)
#[serde(skip_serializing_if = "Option::is_none")]
pub created_before: Option<DateTime<Utc>>,
/// Filter by description text
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Filter by device asset ID
#[serde(skip_serializing_if = "Option::is_none")]
pub device_asset_id: Option<String>,
/// Device ID to filter by
#[serde(skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
/// Filter by favorite status
#[serde(skip_serializing_if = "Option::is_none")]
pub is_favorite: Option<bool>,
/// Filter by motion photo status
#[serde(skip_serializing_if = "Option::is_none")]
pub is_motion: Option<bool>,
/// Filter assets not in any album
#[serde(skip_serializing_if = "Option::is_none")]
pub is_not_in_album: Option<bool>,
/// Filter by offline status
#[serde(skip_serializing_if = "Option::is_none")]
pub is_offline: Option<bool>,
/// Filter by lens model
#[serde(skip_serializing_if = "Option::is_none")]
pub lens_model: Option<String>,
/// Library ID to filter by
#[serde(skip_serializing_if = "Option::is_none")]
pub library_id: Option<AssetId>,
/// Filter by camera make
#[serde(skip_serializing_if = "Option::is_none")]
pub make: Option<String>,
/// Filter by camera model
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Filter by OCR text content
#[serde(skip_serializing_if = "Option::is_none")]
pub ocr: Option<String>,
/// Sort order
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<AssetOrder>,
/// Filter by original file name
#[serde(skip_serializing_if = "Option::is_none")]
pub original_file_name: Option<String>,
/// Page number (default: 1)
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
/// Filter by person IDs
#[serde(skip_serializing_if = "Option::is_none")]
pub person_ids: Option<Vec<AssetId>>,
/// Filter by rating [1-5], or null for unrated (-1 deprecated)
#[serde(skip_serializing_if = "Option::is_none")]
pub rating: Option<i32>,
/// Number of results to return (default: 100)
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u32>,
/// Filter by state/province name
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
/// Filter by tag IDs
#[serde(skip_serializing_if = "Option::is_none")]
pub tag_ids: Option<Vec<AssetId>>,
/// Filter by taken date (after)
#[serde(skip_serializing_if = "Option::is_none")]
pub taken_after: Option<DateTime<Utc>>,
/// Filter by taken date (before)
#[serde(skip_serializing_if = "Option::is_none")]
pub taken_before: Option<DateTime<Utc>>,
/// Asset type filter
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub asset_type: Option<AssetType>,
/// Filter by update date (after)
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_after: Option<DateTime<Utc>>,
/// Filter by update date (before)
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_before: Option<DateTime<Utc>>,
/// Filter by visibility
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<AssetVisibility>,
/// Include deleted assets
#[serde(skip_serializing_if = "Option::is_none")]
pub with_deleted: Option<bool>,
/// Include EXIF data in response
#[serde(skip_serializing_if = "Option::is_none")]
pub with_exif: Option<bool>,
/// Include people data in response
#[serde(skip_serializing_if = "Option::is_none")]
pub with_people: Option<bool>,
/// Include stacked assets
#[serde(skip_serializing_if = "Option::is_none")]
pub with_stacked: Option<bool>,
}
/// Represents a facet value count
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchFacetCount {
/// Number of assets with this facet value
pub count: i64,
/// Facet value
pub value: String,
}
/// Represents a search facet
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchFacet {
/// Facet counts
pub counts: Vec<SearchFacetCount>,
/// Facet field name
pub field_name: String,
}
/// Paginated asset search results
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchAssetResult {
/// Number of assets in this page
pub count: i64,
/// Facet information
pub facets: Vec<SearchFacet>,
/// The actual assets
pub items: Vec<AssetResponse>,
/// Next page token
pub next_page: Option<String>,
/// Total number of matching assets
pub total: i64,
}
/// Paginated album search results
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchAlbumResult {
/// Number of albums in this page
pub count: i64,
/// Facet information
pub facets: Vec<SearchFacet>,
/// The actual albums
pub items: Vec<AlbumResponse>,
/// Total number of matching albums
pub total: i64,
}
/// Combined search response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchResponse {
/// Album results
pub albums: SearchAlbumResult,
/// Asset results
pub assets: SearchAssetResult,
}
#[cfg(test)]
mod tests {
use super::*;