pinakes/crates/pinakes-ui/src/client.rs
NotAShelf 875bdf5ebc
various: bump dependencies; wire up dead code
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I12432bc956453cc4b0a2db82dce1b4976a6a6964
2026-02-09 15:49:22 +03:00

1126 lines
30 KiB
Rust

use anyhow::Result;
use reqwest::Client;
use reqwest::header;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Payload for import events: (path, tag_ids, new_tags, collection_id)
pub type ImportEvent = (String, Vec<String>, Vec<String>, Option<String>);
/// Payload for media update events
#[derive(Debug, Clone, PartialEq)]
pub struct MediaUpdateEvent {
pub id: String,
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub genre: Option<String>,
pub year: Option<i32>,
pub description: Option<String>,
}
#[derive(Clone)]
pub struct ApiClient {
client: Client,
base_url: String,
}
impl PartialEq for ApiClient {
fn eq(&self, other: &Self) -> bool {
self.base_url == other.base_url
}
}
// ── Response types ──
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct MediaResponse {
pub id: String,
pub path: String,
pub file_name: String,
pub media_type: String,
pub content_hash: String,
pub file_size: u64,
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub genre: Option<String>,
pub year: Option<i32>,
pub duration_secs: Option<f64>,
pub description: Option<String>,
#[serde(default)]
pub has_thumbnail: bool,
pub custom_fields: HashMap<String, CustomFieldResponse>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct CustomFieldResponse {
pub field_type: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ImportResponse {
pub media_id: String,
pub was_duplicate: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct BatchImportResponse {
pub results: Vec<BatchImportItemResult>,
pub total: usize,
pub imported: usize,
pub duplicates: usize,
pub errors: usize,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct BatchImportItemResult {
pub path: String,
pub media_id: Option<String>,
pub was_duplicate: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct DirectoryPreviewResponse {
pub files: Vec<DirectoryPreviewFile>,
pub total_count: usize,
pub total_size: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct DirectoryPreviewFile {
pub path: String,
pub file_name: String,
pub media_type: String,
pub file_size: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct DuplicateGroupResponse {
pub content_hash: String,
pub items: Vec<MediaResponse>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct TagResponse {
pub id: String,
pub name: String,
pub parent_id: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct CollectionResponse {
pub id: String,
pub name: String,
pub description: Option<String>,
pub kind: String,
pub filter_query: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct SearchResponse {
pub items: Vec<MediaResponse>,
pub total_count: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct AuditEntryResponse {
pub id: String,
pub media_id: Option<String>,
pub action: String,
pub details: Option<String>,
pub timestamp: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ConfigResponse {
pub backend: String,
pub database_path: Option<String>,
pub roots: Vec<String>,
pub scanning: ScanningConfigResponse,
pub server: ServerConfigResponse,
#[serde(default)]
pub ui: UiConfigResponse,
pub config_path: Option<String>,
pub config_writable: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)]
pub struct UiConfigResponse {
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default = "default_view")]
pub default_view: String,
#[serde(default = "default_page_size")]
pub default_page_size: usize,
#[serde(default = "default_view_mode")]
pub default_view_mode: String,
#[serde(default)]
pub auto_play_media: bool,
#[serde(default = "default_true")]
pub show_thumbnails: bool,
#[serde(default)]
pub sidebar_collapsed: bool,
}
fn default_theme() -> String {
"dark".to_string()
}
fn default_view() -> String {
"library".to_string()
}
fn default_page_size() -> usize {
48
}
fn default_view_mode() -> String {
"grid".to_string()
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct LoginResponse {
pub token: String,
pub username: String,
pub role: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct UserInfoResponse {
pub username: String,
pub role: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ScanningConfigResponse {
pub watch: bool,
pub poll_interval_secs: u64,
pub ignore_patterns: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ServerConfigResponse {
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ScanResponse {
pub files_found: usize,
pub files_processed: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ScanStatusResponse {
pub scanning: bool,
pub files_found: usize,
pub files_processed: usize,
pub error_count: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct BatchOperationResponse {
pub processed: usize,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct LibraryStatisticsResponse {
pub total_media: u64,
pub total_size_bytes: u64,
pub avg_file_size_bytes: u64,
pub media_by_type: Vec<TypeCountResponse>,
pub storage_by_type: Vec<TypeCountResponse>,
pub newest_item: Option<String>,
pub oldest_item: Option<String>,
pub top_tags: Vec<TypeCountResponse>,
pub top_collections: Vec<TypeCountResponse>,
pub total_tags: u64,
pub total_collections: u64,
pub total_duplicates: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct TypeCountResponse {
pub name: String,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ScheduledTaskResponse {
pub id: String,
pub name: String,
pub schedule: String,
pub enabled: bool,
pub last_run: Option<String>,
pub next_run: Option<String>,
pub last_status: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct DatabaseStatsResponse {
pub media_count: u64,
pub tag_count: u64,
pub collection_count: u64,
pub audit_count: u64,
pub database_size_bytes: u64,
pub backend_name: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct SavedSearchResponse {
pub id: String,
pub name: String,
pub query: String,
pub sort_order: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateSavedSearchRequest {
pub name: String,
pub query: String,
pub sort_order: Option<String>,
}
impl ApiClient {
pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
let mut headers = header::HeaderMap::new();
if let Some(key) = api_key
&& let Ok(val) = header::HeaderValue::from_str(&format!("Bearer {key}"))
{
headers.insert(header::AUTHORIZATION, val);
}
let client = Client::builder()
.default_headers(headers)
.build()
.unwrap_or_else(|_| Client::new());
Self {
client,
base_url: base_url.trim_end_matches('/').to_string(),
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
fn url(&self, path: &str) -> String {
format!("{}/api/v1{}", self.base_url, path)
}
pub async fn health_check(&self) -> bool {
match self
.client
.get(self.url("/health"))
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
// ── Media ──
pub async fn list_media(
&self,
offset: u64,
limit: u64,
sort: Option<&str>,
) -> Result<Vec<MediaResponse>> {
let mut params = vec![("offset", offset.to_string()), ("limit", limit.to_string())];
if let Some(s) = sort {
params.push(("sort", s.to_string()));
}
Ok(self
.client
.get(self.url("/media"))
.query(&params)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn get_media(&self, id: &str) -> Result<MediaResponse> {
Ok(self
.client
.get(self.url(&format!("/media/{id}")))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn update_media(&self, event: &MediaUpdateEvent) -> Result<MediaResponse> {
let mut body = serde_json::Map::new();
if let Some(v) = &event.title {
body.insert("title".into(), serde_json::json!(v));
}
if let Some(v) = &event.artist {
body.insert("artist".into(), serde_json::json!(v));
}
if let Some(v) = &event.album {
body.insert("album".into(), serde_json::json!(v));
}
if let Some(v) = &event.genre {
body.insert("genre".into(), serde_json::json!(v));
}
if let Some(v) = event.year {
body.insert("year".into(), serde_json::json!(v));
}
if let Some(v) = &event.description {
body.insert("description".into(), serde_json::json!(v));
}
let id = &event.id;
Ok(self
.client
.patch(self.url(&format!("/media/{id}")))
.json(&serde_json::Value::Object(body))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn delete_media(&self, id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/media/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn open_media(&self, id: &str) -> Result<()> {
self.client
.post(self.url(&format!("/media/{id}/open")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub fn stream_url(&self, id: &str) -> String {
self.url(&format!("/media/{id}/stream"))
}
pub fn thumbnail_url(&self, id: &str) -> String {
self.url(&format!("/media/{id}/thumbnail"))
}
pub async fn get_media_count(&self) -> Result<u64> {
#[derive(Deserialize)]
struct CountResp {
count: u64,
}
let resp: CountResp = self
.client
.get(self.url("/media/count"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp.count)
}
// ── Import ──
pub async fn import_file(&self, path: &str) -> Result<ImportResponse> {
Ok(self
.client
.post(self.url("/media/import"))
.json(&serde_json::json!({"path": path}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn import_with_options(
&self,
path: &str,
tag_ids: &[String],
new_tags: &[String],
collection_id: Option<&str>,
) -> Result<ImportResponse> {
let mut body = serde_json::json!({"path": path});
if !tag_ids.is_empty() {
body["tag_ids"] = serde_json::json!(tag_ids);
}
if !new_tags.is_empty() {
body["new_tags"] = serde_json::json!(new_tags);
}
if let Some(cid) = collection_id {
body["collection_id"] = serde_json::json!(cid);
}
Ok(self
.client
.post(self.url("/media/import/options"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn import_directory(
&self,
path: &str,
tag_ids: &[String],
new_tags: &[String],
collection_id: Option<&str>,
) -> Result<BatchImportResponse> {
let mut body = serde_json::json!({"path": path});
if !tag_ids.is_empty() {
body["tag_ids"] = serde_json::json!(tag_ids);
}
if !new_tags.is_empty() {
body["new_tags"] = serde_json::json!(new_tags);
}
if let Some(cid) = collection_id {
body["collection_id"] = serde_json::json!(cid);
}
Ok(self
.client
.post(self.url("/media/import/directory"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn preview_directory(
&self,
path: &str,
recursive: bool,
) -> Result<DirectoryPreviewResponse> {
Ok(self
.client
.post(self.url("/media/import/preview"))
.json(&serde_json::json!({"path": path, "recursive": recursive}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Search ──
pub async fn search(
&self,
query: &str,
sort: Option<&str>,
offset: u64,
limit: u64,
) -> Result<SearchResponse> {
let mut params = vec![
("q", query.to_string()),
("offset", offset.to_string()),
("limit", limit.to_string()),
];
if let Some(s) = sort {
params.push(("sort", s.to_string()));
}
Ok(self
.client
.get(self.url("/search"))
.query(&params)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Tags ──
pub async fn list_tags(&self) -> Result<Vec<TagResponse>> {
Ok(self
.client
.get(self.url("/tags"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn create_tag(&self, name: &str, parent_id: Option<&str>) -> Result<TagResponse> {
let mut body = serde_json::json!({"name": name});
if let Some(pid) = parent_id {
body["parent_id"] = serde_json::Value::String(pid.to_string());
}
Ok(self
.client
.post(self.url("/tags"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn delete_tag(&self, id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/tags/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn tag_media(&self, media_id: &str, tag_id: &str) -> Result<()> {
self.client
.post(self.url(&format!("/media/{media_id}/tags")))
.json(&serde_json::json!({"tag_id": tag_id}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn untag_media(&self, media_id: &str, tag_id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/media/{media_id}/tags/{tag_id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn get_media_tags(&self, media_id: &str) -> Result<Vec<TagResponse>> {
Ok(self
.client
.get(self.url(&format!("/media/{media_id}/tags")))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Custom Fields ──
pub async fn set_custom_field(
&self,
media_id: &str,
name: &str,
field_type: &str,
value: &str,
) -> Result<()> {
self.client
.post(self.url(&format!("/media/{media_id}/custom-fields")))
.json(&serde_json::json!({"name": name, "field_type": field_type, "value": value}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn delete_custom_field(&self, media_id: &str, name: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/media/{media_id}/custom-fields/{name}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
// ── Collections ──
pub async fn list_collections(&self) -> Result<Vec<CollectionResponse>> {
Ok(self
.client
.get(self.url("/collections"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn create_collection(
&self,
name: &str,
kind: &str,
description: Option<&str>,
filter_query: Option<&str>,
) -> Result<CollectionResponse> {
let mut body = serde_json::json!({"name": name, "kind": kind});
if let Some(desc) = description {
body["description"] = serde_json::Value::String(desc.to_string());
}
if let Some(fq) = filter_query {
body["filter_query"] = serde_json::Value::String(fq.to_string());
}
Ok(self
.client
.post(self.url("/collections"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn delete_collection(&self, id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/collections/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn get_collection_members(&self, id: &str) -> Result<Vec<MediaResponse>> {
Ok(self
.client
.get(self.url(&format!("/collections/{id}/members")))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn add_to_collection(
&self,
collection_id: &str,
media_id: &str,
position: i32,
) -> Result<()> {
self.client
.post(self.url(&format!("/collections/{collection_id}/members")))
.json(&serde_json::json!({"media_id": media_id, "position": position}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn remove_from_collection(&self, collection_id: &str, media_id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/collections/{collection_id}/members/{media_id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
// ── Batch Operations ──
pub async fn batch_tag(
&self,
media_ids: &[String],
tag_ids: &[String],
) -> Result<BatchOperationResponse> {
Ok(self
.client
.post(self.url("/media/batch/tag"))
.json(&serde_json::json!({"media_ids": media_ids, "tag_ids": tag_ids}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn batch_delete(&self, media_ids: &[String]) -> Result<BatchOperationResponse> {
Ok(self
.client
.post(self.url("/media/batch/delete"))
.json(&serde_json::json!({"media_ids": media_ids}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn delete_all_media(&self) -> Result<BatchOperationResponse> {
Ok(self
.client
.delete(self.url("/media/all"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn batch_add_to_collection(
&self,
media_ids: &[String],
collection_id: &str,
) -> Result<BatchOperationResponse> {
Ok(self
.client
.post(self.url("/media/batch/collection"))
.json(&serde_json::json!({"media_ids": media_ids, "collection_id": collection_id}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Audit ──
pub async fn list_audit(&self, offset: u64, limit: u64) -> Result<Vec<AuditEntryResponse>> {
Ok(self
.client
.get(self.url("/audit"))
.query(&[("offset", offset.to_string()), ("limit", limit.to_string())])
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Scan ──
pub async fn trigger_scan(&self) -> Result<Vec<ScanResponse>> {
Ok(self
.client
.post(self.url("/scan"))
.json(&serde_json::json!({"path": serde_json::Value::Null}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn scan_status(&self) -> Result<ScanStatusResponse> {
Ok(self
.client
.get(self.url("/scan/status"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Config ──
pub async fn get_config(&self) -> Result<ConfigResponse> {
Ok(self
.client
.get(self.url("/config"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn update_scanning(
&self,
watch: Option<bool>,
poll_interval: Option<u64>,
ignore_patterns: Option<Vec<String>>,
) -> Result<ConfigResponse> {
let mut body = serde_json::Map::new();
if let Some(w) = watch {
body.insert("watch".into(), serde_json::Value::Bool(w));
}
if let Some(p) = poll_interval {
body.insert("poll_interval_secs".into(), serde_json::json!(p));
}
if let Some(pat) = ignore_patterns {
body.insert("ignore_patterns".into(), serde_json::json!(pat));
}
Ok(self
.client
.put(self.url("/config/scanning"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn add_root(&self, path: &str) -> Result<ConfigResponse> {
Ok(self
.client
.post(self.url("/config/roots"))
.json(&serde_json::json!({"path": path}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn remove_root(&self, path: &str) -> Result<ConfigResponse> {
Ok(self
.client
.delete(self.url("/config/roots"))
.json(&serde_json::json!({"path": path}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Database Management ──
pub async fn database_stats(&self) -> Result<DatabaseStatsResponse> {
Ok(self
.client
.get(self.url("/database/stats"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn vacuum_database(&self) -> Result<()> {
self.client
.post(self.url("/database/vacuum"))
.json(&serde_json::json!({}))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn clear_database(&self) -> Result<()> {
self.client
.post(self.url("/database/clear"))
.json(&serde_json::json!({}))
.send()
.await?
.error_for_status()?;
Ok(())
}
// ── Duplicates ──
pub async fn list_duplicates(&self) -> Result<Vec<DuplicateGroupResponse>> {
Ok(self
.client
.get(self.url("/duplicates"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── UI Config ──
pub async fn update_ui_config(&self, updates: serde_json::Value) -> Result<UiConfigResponse> {
Ok(self
.client
.put(self.url("/config/ui"))
.json(&updates)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Auth ──
pub async fn login(&self, username: &str, password: &str) -> Result<LoginResponse> {
Ok(self
.client
.post(self.url("/auth/login"))
.json(&serde_json::json!({"username": username, "password": password}))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn logout(&self) -> Result<()> {
self.client
.post(self.url("/auth/logout"))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn get_current_user(&self) -> Result<UserInfoResponse> {
Ok(self
.client
.get(self.url("/auth/me"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn library_statistics(&self) -> Result<LibraryStatisticsResponse> {
Ok(self
.client
.get(self.url("/statistics"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn list_scheduled_tasks(&self) -> Result<Vec<ScheduledTaskResponse>> {
Ok(self
.client
.get(self.url("/tasks/scheduled"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn toggle_scheduled_task(&self, id: &str) -> Result<serde_json::Value> {
Ok(self
.client
.post(self.url(&format!("/tasks/scheduled/{}/toggle", id)))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn run_scheduled_task_now(&self, id: &str) -> Result<serde_json::Value> {
Ok(self
.client
.post(self.url(&format!("/tasks/scheduled/{}/run-now", id)))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
// ── Saved Searches ──
pub async fn list_saved_searches(&self) -> Result<Vec<SavedSearchResponse>> {
Ok(self
.client
.get(self.url("/saved-searches"))
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn create_saved_search(
&self,
name: &str,
query: &str,
sort_order: Option<&str>,
) -> Result<SavedSearchResponse> {
let req = CreateSavedSearchRequest {
name: name.to_string(),
query: query.to_string(),
sort_order: sort_order.map(|s| s.to_string()),
};
Ok(self
.client
.post(self.url("/saved-searches"))
.json(&req)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
pub async fn delete_saved_search(&self, id: &str) -> Result<()> {
self.client
.delete(self.url(&format!("/saved-searches/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub fn set_token(&mut self, token: &str) {
let mut headers = header::HeaderMap::new();
if let Ok(val) = header::HeaderValue::from_str(&format!("Bearer {token}")) {
headers.insert(header::AUTHORIZATION, val);
}
self.client = Client::builder()
.default_headers(headers)
.build()
.unwrap_or_else(|_| Client::new());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_url() {
let client = ApiClient::new("http://localhost:3000", None);
assert_eq!(client.base_url(), "http://localhost:3000");
}
#[test]
fn test_stream_url() {
let client = ApiClient::new("http://localhost:3000", None);
let url = client.stream_url("test-id-123");
assert_eq!(url, "http://localhost:3000/api/v1/media/test-id-123/stream");
}
#[test]
fn test_thumbnail_url() {
let client = ApiClient::new("http://localhost:3000", None);
let url = client.thumbnail_url("test-id-456");
assert_eq!(
url,
"http://localhost:3000/api/v1/media/test-id-456/thumbnail"
);
}
#[test]
fn test_client_creation_with_api_key() {
let client = ApiClient::new("http://localhost:3000", Some("test-key"));
assert_eq!(client.base_url(), "http://localhost:3000");
}
#[test]
fn test_base_url_trailing_slash() {
let client = ApiClient::new("http://localhost:3000/", None);
assert_eq!(client.base_url(), "http://localhost:3000");
}
}