pinakes-tui: cover more API routes in the TUI crate

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id14b6f82d3b9f3c27bee9c214a1bdedc6a6a6964
This commit is contained in:
raf 2026-03-21 02:19:55 +03:00
commit 8129c5a6e7
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
8 changed files with 1866 additions and 67 deletions

View file

@ -186,6 +186,62 @@ pub struct AuthorSummary {
pub count: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PlaylistResponse {
pub id: String,
pub name: String,
pub description: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CommentResponse {
pub text: String,
pub created_at: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TranscodeSessionResponse {
pub id: String,
pub profile: String,
pub status: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SubtitleEntry {
pub language: Option<String>,
pub format: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SubtitleListResponse {
pub subtitles: Vec<SubtitleEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DeviceResponse {
pub id: String,
pub name: String,
pub device_type: Option<String>,
pub last_seen: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WebhookInfo {
#[serde(default)]
pub id: String,
pub url: String,
pub events: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct UserResponse {
pub id: String,
pub username: String,
pub role: String,
pub created_at: String,
}
impl ApiClient {
pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
let client = api_key.map_or_else(Client::new, |key| {
@ -198,7 +254,13 @@ impl ApiClient {
Client::builder()
.default_headers(headers)
.build()
.unwrap_or_default()
.unwrap_or_else(|e| {
tracing::warn!(
"failed to build authenticated HTTP client: {e}; falling back to \
unauthenticated client"
);
Client::new()
})
});
Self {
client,
@ -627,4 +689,303 @@ impl ApiClient {
.error_for_status()?;
Ok(())
}
pub async fn list_playlists(&self) -> Result<Vec<PlaylistResponse>> {
let resp = self
.client
.get(self.url("/playlists"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn create_playlist(
&self,
name: &str,
description: Option<&str>,
) -> Result<PlaylistResponse> {
let mut body = serde_json::json!({"name": name});
if let Some(desc) = description {
body["description"] = serde_json::Value::String(desc.to_string());
}
let resp = self
.client
.post(self.url("/playlists"))
.json(&body)
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn delete_playlist(&self, id: &str) -> Result<()> {
self
.client
.delete(self.url(&format!("/playlists/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn get_playlist_items(
&self,
id: &str,
) -> Result<Vec<MediaResponse>> {
let resp = self
.client
.get(self.url(&format!("/playlists/{id}/items")))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
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(())
}
pub async fn shuffle_playlist(&self, id: &str) -> Result<()> {
self
.client
.post(self.url(&format!("/playlists/{id}/shuffle")))
.send()
.await?
.error_for_status()?;
Ok(())
}
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(())
}
pub async fn add_comment(
&self,
media_id: &str,
text: &str,
) -> Result<CommentResponse> {
let resp = self
.client
.post(self.url(&format!("/media/{media_id}/comments")))
.json(&serde_json::json!({"text": text}))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_comments(
&self,
media_id: &str,
) -> Result<Vec<CommentResponse>> {
let resp = self
.client
.get(self.url(&format!("/media/{media_id}/comments")))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn toggle_favorite(&self, media_id: &str) -> Result<()> {
// Try POST to add; if it fails with conflict, DELETE to remove
let post_resp = self
.client
.post(self.url("/favorites"))
.json(&serde_json::json!({"media_id": media_id}))
.send()
.await?;
if post_resp.status() == reqwest::StatusCode::CONFLICT
|| post_resp.status() == reqwest::StatusCode::UNPROCESSABLE_ENTITY
{
// Already a favorite: remove it
self
.client
.delete(self.url(&format!("/favorites/{media_id}")))
.send()
.await?
.error_for_status()?;
} else {
post_resp.error_for_status()?;
}
Ok(())
}
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(())
}
pub async fn start_transcode(
&self,
media_id: &str,
profile: &str,
) -> Result<TranscodeSessionResponse> {
let resp = self
.client
.post(self.url(&format!("/media/{media_id}/transcode")))
.json(&serde_json::json!({"profile": profile}))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_transcodes(&self) -> Result<Vec<TranscodeSessionResponse>> {
let resp = self
.client
.get(self.url("/transcode"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn cancel_transcode(&self, id: &str) -> Result<()> {
self
.client
.delete(self.url(&format!("/transcode/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn list_subtitles(
&self,
media_id: &str,
) -> Result<SubtitleListResponse> {
let resp = self
.client
.get(self.url(&format!("/media/{media_id}/subtitles")))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_sync_devices(&self) -> Result<Vec<DeviceResponse>> {
let resp = self
.client
.get(self.url("/sync/devices"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
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(())
}
pub async fn list_webhooks(&self) -> Result<Vec<WebhookInfo>> {
let resp = self
.client
.get(self.url("/webhooks"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
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(())
}
pub async fn list_users(&self) -> Result<Vec<UserResponse>> {
let resp = self
.client
.get(self.url("/users"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn delete_user(&self, id: &str) -> Result<()> {
self
.client
.delete(self.url(&format!("/users/{id}")))
.send()
.await?
.error_for_status()?;
Ok(())
}
pub async fn add_to_playlist(
&self,
playlist_id: &str,
media_id: &str,
) -> Result<()> {
let body = serde_json::json!({"media_id": media_id});
let resp = self
.client
.post(self.url(&format!("/playlists/{playlist_id}/items")))
.json(&body)
.send()
.await?;
if resp.status().is_success() {
Ok(())
} else {
anyhow::bail!("add to playlist failed: {}", resp.status())
}
}
}