Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I4a6b498153eccd5407510dd541b7f4816a6a6964
455 lines
12 KiB
Rust
455 lines
12 KiB
Rust
use anyhow::Result;
|
|
use reqwest::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Clone)]
|
|
pub struct ApiClient {
|
|
client: Client,
|
|
base_url: String,
|
|
}
|
|
|
|
// Response types (mirror server DTOs)
|
|
#[derive(Debug, Clone, 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, Deserialize, Serialize)]
|
|
pub struct CustomFieldResponse {
|
|
pub field_type: String,
|
|
pub value: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct ImportResponse {
|
|
pub media_id: String,
|
|
pub was_duplicate: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct TagResponse {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub parent_id: Option<String>,
|
|
pub created_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, 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, Deserialize)]
|
|
pub struct SearchResponse {
|
|
pub items: Vec<MediaResponse>,
|
|
pub total_count: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, 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, Deserialize)]
|
|
pub struct ScanResponse {
|
|
pub files_found: usize,
|
|
pub files_processed: usize,
|
|
pub errors: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, 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, Deserialize)]
|
|
pub struct DuplicateGroupResponse {
|
|
pub content_hash: String,
|
|
pub items: Vec<MediaResponse>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct JobResponse {
|
|
pub id: String,
|
|
pub kind: serde_json::Value,
|
|
pub status: serde_json::Value,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, 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, Deserialize)]
|
|
pub struct LibraryStatisticsResponse {
|
|
pub total_media: u64,
|
|
pub total_size_bytes: u64,
|
|
pub avg_file_size_bytes: u64,
|
|
pub media_by_type: Vec<TypeCount>,
|
|
pub storage_by_type: Vec<TypeCount>,
|
|
pub newest_item: Option<String>,
|
|
pub oldest_item: Option<String>,
|
|
pub top_tags: Vec<TypeCount>,
|
|
pub top_collections: Vec<TypeCount>,
|
|
pub total_tags: u64,
|
|
pub total_collections: u64,
|
|
pub total_duplicates: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct TypeCount {
|
|
pub name: String,
|
|
pub count: u64,
|
|
}
|
|
|
|
impl ApiClient {
|
|
pub fn new(base_url: &str) -> Self {
|
|
Self {
|
|
client: Client::new(),
|
|
base_url: base_url.trim_end_matches('/').to_string(),
|
|
}
|
|
}
|
|
|
|
fn url(&self, path: &str) -> String {
|
|
format!("{}/api/v1{}", self.base_url, path)
|
|
}
|
|
|
|
pub async fn list_media(&self, offset: u64, limit: u64) -> Result<Vec<MediaResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/media"))
|
|
.query(&[("offset", offset.to_string()), ("limit", limit.to_string())])
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn get_media(&self, id: &str) -> Result<MediaResponse> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url(&format!("/media/{id}")))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn import_file(&self, path: &str) -> Result<ImportResponse> {
|
|
let resp = self
|
|
.client
|
|
.post(self.url("/media/import"))
|
|
.json(&serde_json::json!({"path": path}))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
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 async fn search(&self, query: &str, offset: u64, limit: u64) -> Result<SearchResponse> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/search"))
|
|
.query(&[
|
|
("q", query.to_string()),
|
|
("offset", offset.to_string()),
|
|
("limit", limit.to_string()),
|
|
])
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn list_tags(&self) -> Result<Vec<TagResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/tags"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
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());
|
|
}
|
|
let resp = self
|
|
.client
|
|
.post(self.url("/tags"))
|
|
.json(&body)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
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>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url(&format!("/media/{media_id}/tags")))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn list_collections(&self) -> Result<Vec<CollectionResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/collections"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
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 trigger_scan(&self, path: Option<&str>) -> Result<Vec<ScanResponse>> {
|
|
let body = match path {
|
|
Some(p) => serde_json::json!({"path": p}),
|
|
None => serde_json::json!({"path": null}),
|
|
};
|
|
let resp = self
|
|
.client
|
|
.post(self.url("/scan"))
|
|
.json(&body)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn list_audit(&self, offset: u64, limit: u64) -> Result<Vec<AuditEntryResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/audit"))
|
|
.query(&[("offset", offset.to_string()), ("limit", limit.to_string())])
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn find_duplicates(&self) -> Result<Vec<DuplicateGroupResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/duplicates"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn database_stats(&self) -> Result<DatabaseStatsResponse> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/database/stats"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn list_jobs(&self) -> Result<Vec<JobResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/jobs"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
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 update_media(
|
|
&self,
|
|
id: &str,
|
|
updates: serde_json::Value,
|
|
) -> Result<MediaResponse> {
|
|
let resp = self
|
|
.client
|
|
.patch(self.url(&format!("/media/{id}")))
|
|
.json(&updates)
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn library_statistics(&self) -> Result<LibraryStatisticsResponse> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/statistics"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn list_scheduled_tasks(&self) -> Result<Vec<ScheduledTaskResponse>> {
|
|
let resp = self
|
|
.client
|
|
.get(self.url("/tasks/scheduled"))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?
|
|
.json()
|
|
.await?;
|
|
Ok(resp)
|
|
}
|
|
|
|
pub async fn toggle_scheduled_task(&self, id: &str) -> Result<()> {
|
|
self.client
|
|
.post(self.url(&format!("/tasks/scheduled/{id}/toggle")))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn run_task_now(&self, id: &str) -> Result<()> {
|
|
self.client
|
|
.post(self.url(&format!("/tasks/scheduled/{id}/run-now")))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
Ok(())
|
|
}
|
|
}
|