diff --git a/crates/pinakes-ui/src/client.rs b/crates/pinakes-ui/src/client.rs index a82283a..356faec 100644 --- a/crates/pinakes-ui/src/client.rs +++ b/crates/pinakes-ui/src/client.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; /// Payload for import events: (path, tag_ids, new_tags, collection_id) pub type ImportEvent = (String, Vec, Vec, Option); -/// Payload for media update events +/// Fields that can be updated on a media item. #[derive(Debug, Clone, PartialEq)] pub struct MediaUpdateEvent { pub id: String, @@ -18,6 +18,7 @@ pub struct MediaUpdateEvent { pub description: Option, } +/// HTTP client for the Pinakes server API. pub struct ApiClient { client: Client, base_url: String, @@ -46,8 +47,7 @@ impl PartialEq for ApiClient { } } -// Response types - +/// A media item returned by the API. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct MediaResponse { pub id: String, @@ -72,18 +72,21 @@ pub struct MediaResponse { pub links_extracted_at: Option, } +/// A single custom metadata field on a media item. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CustomFieldResponse { pub field_type: String, pub value: String, } +/// Result of a single file import. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ImportResponse { pub media_id: String, pub was_duplicate: bool, } +/// Summary of a batch import operation. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct BatchImportResponse { pub results: Vec, @@ -93,6 +96,7 @@ pub struct BatchImportResponse { pub errors: usize, } +/// Per-file result within a batch import. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct BatchImportItemResult { pub path: String, @@ -101,6 +105,7 @@ pub struct BatchImportItemResult { pub error: Option, } +/// Preview of files in a directory before import. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct DirectoryPreviewResponse { pub files: Vec, @@ -108,6 +113,7 @@ pub struct DirectoryPreviewResponse { pub total_size: u64, } +/// A single file entry in a directory preview. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct DirectoryPreviewFile { pub path: String, @@ -116,12 +122,14 @@ pub struct DirectoryPreviewFile { pub file_size: u64, } +/// A group of media items sharing the same content hash. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct DuplicateGroupResponse { pub content_hash: String, pub items: Vec, } +/// A tag returned by the API. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct TagResponse { pub id: String, @@ -130,6 +138,7 @@ pub struct TagResponse { pub created_at: String, } +/// A collection returned by the API. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct CollectionResponse { pub id: String, @@ -141,12 +150,14 @@ pub struct CollectionResponse { pub updated_at: String, } +/// Paginated search results. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct SearchResponse { pub items: Vec, pub total_count: u64, } +/// A single audit log entry. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct AuditEntryResponse { pub id: String, @@ -156,6 +167,7 @@ pub struct AuditEntryResponse { pub timestamp: String, } +/// Full server configuration. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ConfigResponse { pub backend: String, @@ -169,6 +181,7 @@ pub struct ConfigResponse { pub config_writable: bool, } +/// UI-specific configuration. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Default)] pub struct UiConfigResponse { #[serde(default = "default_theme")] @@ -203,6 +216,7 @@ fn default_true() -> bool { true } +/// Response returned after a successful login. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct LoginResponse { pub token: String, @@ -210,12 +224,14 @@ pub struct LoginResponse { pub role: String, } +/// Basic identity information for the authenticated user. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct UserInfoResponse { pub username: String, pub role: String, } +/// Scanning configuration section. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ScanningConfigResponse { pub watch: bool, @@ -223,12 +239,14 @@ pub struct ScanningConfigResponse { pub ignore_patterns: Vec, } +/// Server bind address configuration. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ServerConfigResponse { pub host: String, pub port: u16, } +/// Summary of a completed scan. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ScanResponse { pub files_found: usize, @@ -236,6 +254,7 @@ pub struct ScanResponse { pub errors: Vec, } +/// Live status of an ongoing or recent scan. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ScanStatusResponse { pub scanning: bool, @@ -245,12 +264,14 @@ pub struct ScanStatusResponse { pub errors: Vec, } +/// Result of a batch operation on media items. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct BatchOperationResponse { pub processed: usize, pub errors: Vec, } +/// Aggregate library statistics. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct LibraryStatisticsResponse { pub total_media: u64, @@ -267,12 +288,14 @@ pub struct LibraryStatisticsResponse { pub total_duplicates: u64, } +/// A named count entry used in statistics breakdowns. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct TypeCountResponse { pub name: String, pub count: u64, } +/// A scheduled background task. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ScheduledTaskResponse { pub id: String, @@ -284,6 +307,7 @@ pub struct ScheduledTaskResponse { pub last_status: Option, } +/// Low-level database statistics. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct DatabaseStatsResponse { pub media_count: u64, @@ -294,14 +318,14 @@ pub struct DatabaseStatsResponse { pub backend_name: String, } -// Markdown notes/links response types - +/// Incoming backlinks for a markdown note. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct BacklinksResponse { pub backlinks: Vec, pub count: usize, } +/// A single incoming link to a note. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct BacklinkItem { pub link_id: String, @@ -314,12 +338,14 @@ pub struct BacklinkItem { pub link_type: String, } +/// Outgoing links from a markdown note. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct OutgoingLinksResponse { pub links: Vec, pub count: usize, } +/// A single outgoing link from a note. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct OutgoingLinkItem { pub id: String, @@ -331,6 +357,7 @@ pub struct OutgoingLinkItem { pub is_resolved: bool, } +/// Note graph data for visualization. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct GraphResponse { pub nodes: Vec, @@ -339,6 +366,7 @@ pub struct GraphResponse { pub edge_count: usize, } +/// A node in the note link graph. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct GraphNodeResponse { pub id: String, @@ -349,6 +377,7 @@ pub struct GraphNodeResponse { pub backlink_count: u32, } +/// A directed edge in the note link graph. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct GraphEdgeResponse { pub source: String, @@ -356,12 +385,14 @@ pub struct GraphEdgeResponse { pub link_type: String, } +/// Result of a link re-extraction operation. #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ReindexLinksResponse { pub message: String, pub links_extracted: usize, } +/// A saved search query. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SavedSearchResponse { pub id: String, @@ -371,6 +402,7 @@ pub struct SavedSearchResponse { pub created_at: chrono::DateTime, } +/// Request body for creating a saved search. #[derive(Debug, Clone, Serialize)] pub struct CreateSavedSearchRequest { pub name: String, @@ -378,8 +410,7 @@ pub struct CreateSavedSearchRequest { pub sort_order: Option, } -// Book management response types - +/// Book-specific metadata for a media item. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct BookMetadataResponse { pub media_id: String, @@ -397,6 +428,7 @@ pub struct BookMetadataResponse { pub identifiers: FxHashMap>, } +/// An author associated with a book. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct BookAuthorResponse { pub name: String, @@ -405,6 +437,7 @@ pub struct BookAuthorResponse { pub position: i32, } +/// Reading progress for a book. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct ReadingProgressResponse { pub media_id: String, @@ -415,19 +448,111 @@ pub struct ReadingProgressResponse { pub last_read_at: String, } +/// A book series with its total book count. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SeriesSummary { pub name: String, pub book_count: u64, } +/// An author with their total book count. #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct AuthorSummary { pub name: String, pub book_count: u64, } +/// A playlist. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct PlaylistResponse { + pub id: String, + pub name: String, + pub description: Option, + pub item_count: Option, +} + +/// An active or completed transcode session. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct TranscodeSessionResponse { + pub id: String, + pub media_id: String, + pub profile: String, + pub status: String, + pub progress: Option, +} + +/// A subtitle file or stream attached to a media item. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SubtitleEntry { + pub id: String, + pub language: Option, + pub format: String, + pub is_embedded: bool, + pub offset_ms: i64, +} + +/// A subtitle track discovered in a media container. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SubtitleTrackInfo { + pub index: u32, + pub language: Option, + pub format: String, + pub title: Option, +} + +/// All subtitle information for a media item. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct SubtitleListResponse { + pub subtitles: Vec, + pub available_tracks: Vec, +} + +/// A star rating on a media item. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct RatingResponse { + pub media_id: String, + pub stars: u8, + pub user_id: Option, +} + +/// A user comment on a media item. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct CommentResponse { + pub id: String, + pub media_id: String, + pub text: String, + pub created_at: Option, +} + +/// A registered sync device. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct DeviceResponse { + pub id: String, + pub name: String, + pub device_type: String, + pub last_seen: Option, +} + +/// A registered webhook endpoint. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct WebhookResponse { + pub id: String, + pub url: String, + pub events: Vec, +} + +/// A user account. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct UserResponse { + pub id: String, + pub username: String, + pub role: String, + pub created_at: Option, +} + impl ApiClient { + /// Create a new client targeting `base_url`, optionally authenticating with + /// a bearer token. pub fn new(base_url: &str, api_key: Option<&str>) -> Self { let mut headers = header::HeaderMap::new(); if let Some(key) = api_key @@ -445,6 +570,7 @@ impl ApiClient { } } + /// Return the base URL without a trailing slash. pub fn base_url(&self) -> &str { &self.base_url } @@ -453,6 +579,7 @@ impl ApiClient { format!("{}/api/v1{}", self.base_url, path) } + /// Return `true` if the server responds to a health check within 3 seconds. pub async fn health_check(&self) -> bool { match self .client @@ -466,8 +593,7 @@ impl ApiClient { } } - // Media - + /// List media items with optional sorting. pub async fn list_media( &self, offset: u64, @@ -492,6 +618,7 @@ impl ApiClient { ) } + /// Fetch a single media item by ID. pub async fn get_media(&self, id: &str) -> Result { Ok( self @@ -505,6 +632,7 @@ impl ApiClient { ) } + /// Apply metadata updates to a media item. pub async fn update_media( &self, event: &MediaUpdateEvent, @@ -542,6 +670,7 @@ impl ApiClient { ) } + /// Delete a media item by ID. pub async fn delete_media(&self, id: &str) -> Result<()> { self .client @@ -552,6 +681,7 @@ impl ApiClient { Ok(()) } + /// Ask the server to open a media item in the system default application. pub async fn open_media(&self, id: &str) -> Result<()> { self .client @@ -562,14 +692,17 @@ impl ApiClient { Ok(()) } + /// Return the streaming URL for a media item. pub fn stream_url(&self, id: &str) -> String { self.url(&format!("/media/{id}/stream")) } + /// Return the thumbnail URL for a media item. pub fn thumbnail_url(&self, id: &str) -> String { self.url(&format!("/media/{id}/thumbnail")) } + /// Return the total number of media items in the library. pub async fn get_media_count(&self) -> Result { #[derive(Deserialize)] struct CountResp { @@ -586,8 +719,7 @@ impl ApiClient { Ok(resp.count) } - // Import - + /// Import a single file into the library. pub async fn import_file(&self, path: &str) -> Result { Ok( self @@ -602,6 +734,7 @@ impl ApiClient { ) } + /// Import a file with tags and an optional collection assignment. pub async fn import_with_options( &self, path: &str, @@ -632,6 +765,8 @@ impl ApiClient { ) } + /// Import all files in a directory, optionally assigning tags and a + /// collection. pub async fn import_directory( &self, path: &str, @@ -662,6 +797,7 @@ impl ApiClient { ) } + /// List files in a directory without importing them. pub async fn preview_directory( &self, path: &str, @@ -680,8 +816,7 @@ impl ApiClient { ) } - // Search - + /// Run a full-text search query. pub async fn search( &self, query: &str, @@ -710,8 +845,7 @@ impl ApiClient { ) } - // Tags - + /// List all tags. pub async fn list_tags(&self) -> Result> { Ok( self @@ -725,6 +859,7 @@ impl ApiClient { ) } + /// Create a tag, optionally nested under a parent. pub async fn create_tag( &self, name: &str, @@ -747,6 +882,7 @@ impl ApiClient { ) } + /// Delete a tag by ID. pub async fn delete_tag(&self, id: &str) -> Result<()> { self .client @@ -757,6 +893,7 @@ impl ApiClient { Ok(()) } + /// Apply a tag to a media item. pub async fn tag_media(&self, media_id: &str, tag_id: &str) -> Result<()> { self .client @@ -768,6 +905,7 @@ impl ApiClient { Ok(()) } + /// Remove a tag from a media item. pub async fn untag_media(&self, media_id: &str, tag_id: &str) -> Result<()> { self .client @@ -778,6 +916,7 @@ impl ApiClient { Ok(()) } + /// List all tags applied to a media item. pub async fn get_media_tags( &self, media_id: &str, @@ -794,8 +933,7 @@ impl ApiClient { ) } - // Custom fields - + /// Set a custom metadata field on a media item. pub async fn set_custom_field( &self, media_id: &str, @@ -812,6 +950,7 @@ impl ApiClient { Ok(()) } + /// Delete a custom metadata field from a media item. pub async fn delete_custom_field( &self, media_id: &str, @@ -826,8 +965,7 @@ impl ApiClient { Ok(()) } - // Collections - + /// List all collections. pub async fn list_collections(&self) -> Result> { Ok( self @@ -841,6 +979,7 @@ impl ApiClient { ) } + /// Create a collection. pub async fn create_collection( &self, name: &str, @@ -868,6 +1007,7 @@ impl ApiClient { ) } + /// Delete a collection by ID. pub async fn delete_collection(&self, id: &str) -> Result<()> { self .client @@ -878,6 +1018,7 @@ impl ApiClient { Ok(()) } + /// List media items that are members of a collection. pub async fn get_collection_members( &self, id: &str, @@ -894,6 +1035,7 @@ impl ApiClient { ) } + /// Add a media item to a collection at the given position. pub async fn add_to_collection( &self, collection_id: &str, @@ -910,6 +1052,7 @@ impl ApiClient { Ok(()) } + /// Remove a media item from a collection. pub async fn remove_from_collection( &self, collection_id: &str, @@ -926,8 +1069,7 @@ impl ApiClient { Ok(()) } - // Batch operations - + /// Apply tags to multiple media items in one request. pub async fn batch_tag( &self, media_ids: &[String], @@ -946,6 +1088,7 @@ impl ApiClient { ) } + /// Delete multiple media items in one request. pub async fn batch_delete( &self, media_ids: &[String], @@ -963,6 +1106,7 @@ impl ApiClient { ) } + /// Delete every media item in the library. pub async fn delete_all_media(&self) -> Result { Ok( self @@ -976,6 +1120,7 @@ impl ApiClient { ) } + /// Add multiple media items to a collection in one request. pub async fn batch_add_to_collection( &self, media_ids: &[String], @@ -992,8 +1137,7 @@ impl ApiClient { .await?) } - // Audit - + /// List audit log entries. pub async fn list_audit( &self, offset: u64, @@ -1012,8 +1156,7 @@ impl ApiClient { ) } - // Scan - + /// Trigger a full library scan. pub async fn trigger_scan(&self) -> Result> { Ok( self @@ -1028,6 +1171,7 @@ impl ApiClient { ) } + /// Return the current scan status. pub async fn scan_status(&self) -> Result { Ok( self @@ -1041,8 +1185,7 @@ impl ApiClient { ) } - // Config - + /// Fetch the full server configuration. pub async fn get_config(&self) -> Result { Ok( self @@ -1056,6 +1199,7 @@ impl ApiClient { ) } + /// Update scanning settings. pub async fn update_scanning( &self, watch: Option, @@ -1085,6 +1229,7 @@ impl ApiClient { ) } + /// Add a root directory to the library. pub async fn add_root(&self, path: &str) -> Result { Ok( self @@ -1099,6 +1244,7 @@ impl ApiClient { ) } + /// Remove a root directory from the library. pub async fn remove_root(&self, path: &str) -> Result { Ok( self @@ -1113,8 +1259,7 @@ impl ApiClient { ) } - // Database management - + /// Fetch database statistics. pub async fn database_stats(&self) -> Result { Ok( self @@ -1128,6 +1273,7 @@ impl ApiClient { ) } + /// Run a VACUUM on the database. pub async fn vacuum_database(&self) -> Result<()> { self .client @@ -1139,6 +1285,7 @@ impl ApiClient { Ok(()) } + /// Clear all data from the database. pub async fn clear_database(&self) -> Result<()> { self .client @@ -1164,8 +1311,7 @@ impl ApiClient { Ok(()) } - // Books - + /// Fetch book-specific metadata for a media item. pub async fn get_book_metadata( &self, media_id: &str, @@ -1182,6 +1328,7 @@ impl ApiClient { ) } + /// List books, optionally filtered by author or series. pub async fn list_books( &self, offset: u64, @@ -1208,6 +1355,7 @@ impl ApiClient { ) } + /// List all series with their book counts. pub async fn list_series(&self) -> Result> { Ok( self @@ -1221,6 +1369,7 @@ impl ApiClient { ) } + /// List books in a series ordered by series index. pub async fn get_series_books( &self, series_name: &str, @@ -1240,6 +1389,7 @@ impl ApiClient { ) } + /// List all authors with their book counts. pub async fn list_authors(&self) -> Result> { Ok( self @@ -1253,6 +1403,7 @@ impl ApiClient { ) } + /// List books by a specific author. pub async fn get_author_books( &self, author_name: &str, @@ -1272,6 +1423,7 @@ impl ApiClient { ) } + /// Fetch reading progress for a book. pub async fn get_reading_progress( &self, media_id: &str, @@ -1288,6 +1440,7 @@ impl ApiClient { ) } + /// Update the current page for a book. pub async fn update_reading_progress( &self, media_id: &str, @@ -1303,6 +1456,7 @@ impl ApiClient { Ok(()) } + /// Fetch the user's reading list, optionally filtered by status. pub async fn get_reading_list( &self, status: Option<&str>, @@ -1323,8 +1477,7 @@ impl ApiClient { ) } - // Duplicates - + /// List all duplicate groups. pub async fn list_duplicates(&self) -> Result> { Ok( self @@ -1338,8 +1491,7 @@ impl ApiClient { ) } - // UI config - + /// Persist UI configuration updates. pub async fn update_ui_config( &self, updates: serde_json::Value, @@ -1357,8 +1509,7 @@ impl ApiClient { ) } - // Auth - + /// Authenticate with the server and obtain a JWT token. pub async fn login( &self, username: &str, @@ -1377,6 +1528,7 @@ impl ApiClient { ) } + /// Invalidate the current session on the server. pub async fn logout(&self) -> Result<()> { self .client @@ -1387,6 +1539,7 @@ impl ApiClient { Ok(()) } + /// Return identity information for the authenticated user. pub async fn get_current_user(&self) -> Result { Ok( self @@ -1400,6 +1553,7 @@ impl ApiClient { ) } + /// Fetch aggregate library statistics. pub async fn library_statistics(&self) -> Result { Ok( self @@ -1413,6 +1567,7 @@ impl ApiClient { ) } + /// List all scheduled background tasks. pub async fn list_scheduled_tasks( &self, ) -> Result> { @@ -1428,6 +1583,7 @@ impl ApiClient { ) } + /// Toggle a scheduled task on or off. pub async fn toggle_scheduled_task( &self, id: &str, @@ -1444,6 +1600,7 @@ impl ApiClient { ) } + /// Execute a scheduled task immediately. pub async fn run_scheduled_task_now( &self, id: &str, @@ -1460,8 +1617,7 @@ impl ApiClient { ) } - // Saved searches - + /// List all saved searches. pub async fn list_saved_searches(&self) -> Result> { Ok( self @@ -1475,6 +1631,7 @@ impl ApiClient { ) } + /// Create a new saved search. pub async fn create_saved_search( &self, name: &str, @@ -1499,6 +1656,7 @@ impl ApiClient { ) } + /// Delete a saved search by ID. pub async fn delete_saved_search(&self, id: &str) -> Result<()> { self .client @@ -1509,8 +1667,6 @@ impl ApiClient { Ok(()) } - // Markdown notes/links - /// Get backlinks (incoming links) to a media item. pub async fn get_backlinks(&self, id: &str) -> Result { Ok( @@ -1603,6 +1759,7 @@ impl ApiClient { Ok(resp.count) } + /// Replace the bearer token used for subsequent requests. 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}")) { @@ -1713,6 +1870,453 @@ impl ApiClient { Ok(()) } + /// List all playlists. + pub async fn list_playlists(&self) -> Result> { + Ok( + self + .client + .get(self.url("/playlists")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Create a new playlist. + pub async fn create_playlist( + &self, + name: &str, + description: Option<&str>, + ) -> Result { + let mut body = serde_json::json!({ "name": name }); + if let Some(desc) = description { + body["description"] = serde_json::Value::String(desc.to_string()); + } + Ok( + self + .client + .post(self.url("/playlists")) + .json(&body) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Update a playlist's name. + pub async fn update_playlist( + &self, + id: &str, + name: &str, + ) -> Result { + Ok( + self + .client + .patch(self.url(&format!("/playlists/{id}"))) + .json(&serde_json::json!({ "name": name })) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Delete a playlist. + pub async fn delete_playlist(&self, id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/playlists/{id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Get items in a playlist. + pub async fn get_playlist_items( + &self, + id: &str, + ) -> Result> { + Ok( + self + .client + .get(self.url(&format!("/playlists/{id}/items"))) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Add a media item to a playlist. + pub async fn add_to_playlist( + &self, + playlist_id: &str, + media_id: &str, + ) -> Result<()> { + self + .client + .post(self.url(&format!("/playlists/{playlist_id}/items"))) + .json(&serde_json::json!({ "media_id": media_id })) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Remove a media item from a playlist. + pub async fn remove_from_playlist( + &self, + playlist_id: &str, + media_id: &str, + ) -> Result<()> { + self + .client + .delete(self.url(&format!("/playlists/{playlist_id}/items/{media_id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Shuffle a playlist. + pub async fn shuffle_playlist(&self, id: &str) -> Result<()> { + self + .client + .post(self.url(&format!("/playlists/{id}/shuffle"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Trigger metadata enrichment for a media item. + pub async fn enrich_media(&self, media_id: &str) -> Result<()> { + self + .client + .post(self.url(&format!("/media/{media_id}/enrich"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Get external metadata for a media item. + pub async fn get_external_metadata( + &self, + media_id: &str, + ) -> Result { + Ok( + self + .client + .get(self.url(&format!("/media/{media_id}/external-metadata"))) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Start a transcode job for a media item. + pub async fn start_transcode( + &self, + media_id: &str, + profile: &str, + ) -> Result { + Ok( + self + .client + .post(self.url(&format!("/media/{media_id}/transcode"))) + .json(&serde_json::json!({ "profile": profile })) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// List all transcode sessions. + pub async fn list_transcodes(&self) -> Result> { + Ok( + self + .client + .get(self.url("/transcode")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Cancel a transcode session. + pub async fn cancel_transcode(&self, id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/transcode/{id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// List subtitles for a media item. + pub async fn list_subtitles_for_media( + &self, + media_id: &str, + ) -> Result { + Ok( + self + .client + .get(self.url(&format!("/media/{media_id}/subtitles"))) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Delete a subtitle entry. + pub async fn delete_subtitle(&self, id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/subtitles/{id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Rate a media item. + pub async fn rate_media(&self, media_id: &str, stars: u8) -> Result<()> { + self + .client + .post(self.url(&format!("/media/{media_id}/ratings"))) + .json(&serde_json::json!({ "stars": stars })) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Get ratings for a media item. + pub async fn get_ratings( + &self, + media_id: &str, + ) -> Result> { + Ok( + self + .client + .get(self.url(&format!("/media/{media_id}/ratings"))) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Add a comment to a media item. + pub async fn add_comment( + &self, + media_id: &str, + text: &str, + ) -> Result { + Ok( + self + .client + .post(self.url(&format!("/media/{media_id}/comments"))) + .json(&serde_json::json!({ "text": text })) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// List comments for a media item. + pub async fn list_comments( + &self, + media_id: &str, + ) -> Result> { + Ok( + self + .client + .get(self.url(&format!("/media/{media_id}/comments"))) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Add a media item to favorites. + pub async fn add_favorite(&self, media_id: &str) -> Result<()> { + self + .client + .post(self.url("/favorites")) + .json(&serde_json::json!({ "media_id": media_id })) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// Remove a media item from favorites. + pub async fn remove_favorite(&self, media_id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/favorites/{media_id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// List favorite media items. + pub async fn list_favorites(&self) -> Result> { + Ok( + self + .client + .get(self.url("/favorites")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// List sync devices. + pub async fn list_sync_devices(&self) -> Result> { + Ok( + self + .client + .get(self.url("/sync/devices")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Delete a sync device. + pub async fn delete_sync_device(&self, id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/sync/devices/{id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// List webhooks. + pub async fn list_webhooks(&self) -> Result> { + Ok( + self + .client + .get(self.url("/webhooks")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Test a webhook. + pub async fn test_webhook(&self, id: &str) -> Result<()> { + self + .client + .post(self.url("/webhooks/test")) + .json(&serde_json::json!({ "id": id })) + .send() + .await? + .error_for_status()?; + Ok(()) + } + + /// List all users. + pub async fn list_users(&self) -> Result> { + Ok( + self + .client + .get(self.url("/users")) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Create a new user. + pub async fn create_user( + &self, + username: &str, + password: &str, + role: &str, + ) -> Result { + Ok( + self + .client + .post(self.url("/users")) + .json(&serde_json::json!({ + "username": username, + "password": password, + "role": role, + })) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Update a user's role. + pub async fn update_user( + &self, + id: &str, + role: &str, + ) -> Result { + Ok( + self + .client + .patch(self.url(&format!("/users/{id}"))) + .json(&serde_json::json!({ "role": role })) + .send() + .await? + .error_for_status()? + .json() + .await?, + ) + } + + /// Delete a user. + pub async fn delete_user(&self, id: &str) -> Result<()> { + self + .client + .delete(self.url(&format!("/users/{id}"))) + .send() + .await? + .error_for_status()?; + Ok(()) + } + /// Make a raw HTTP request to an API path. /// /// The `path` is appended to the base URL without any prefix.