pinakes-tui: add book management view and api key authentication

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I20f205d9e06a93a89e8f4433ed6f80576a6a6964
This commit is contained in:
raf 2026-03-08 00:42:34 +03:00
commit 66861b8a20
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
18 changed files with 917 additions and 251 deletions

View file

@ -145,10 +145,64 @@ pub struct TypeCount {
pub count: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BookMetadataResponse {
pub media_id: String,
pub title: Option<String>,
pub subtitle: Option<String>,
pub publisher: Option<String>,
pub language: Option<String>,
pub isbn: Option<String>,
pub isbn13: Option<String>,
pub page_count: Option<i32>,
pub series: Option<String>,
pub series_index: Option<f64>,
pub authors: Vec<BookAuthorResponse>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BookAuthorResponse {
pub name: String,
pub role: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReadingProgressResponse {
pub media_id: String,
pub current_page: i32,
pub total_pages: Option<i32>,
pub status: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeriesSummary {
pub name: String,
pub count: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AuthorSummary {
pub name: String,
pub count: u64,
}
impl ApiClient {
pub fn new(base_url: &str) -> Self {
pub fn new(base_url: &str, api_key: Option<&str>) -> Self {
let client = api_key.map_or_else(Client::new, |key| {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(val) =
reqwest::header::HeaderValue::from_str(&format!("Bearer {key}"))
{
headers.insert(reqwest::header::AUTHORIZATION, val);
}
Client::builder()
.default_headers(headers)
.build()
.unwrap_or_default()
});
Self {
client: Client::new(),
client,
base_url: base_url.trim_end_matches('/').to_string(),
}
}
@ -346,10 +400,10 @@ impl ApiClient {
&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 body = path.map_or_else(
|| serde_json::json!({"path": null}),
|p| serde_json::json!({"path": p}),
);
let resp = self
.client
.post(self.url("/scan"))
@ -488,4 +542,90 @@ impl ApiClient {
.error_for_status()?;
Ok(())
}
pub async fn get_book_metadata(
&self,
media_id: &str,
) -> Result<BookMetadataResponse> {
let resp = self
.client
.get(self.url(&format!("/books/{media_id}/metadata")))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_books(
&self,
offset: u64,
limit: u64,
) -> Result<Vec<MediaResponse>> {
let resp = self
.client
.get(self.url("/books"))
.query(&[("offset", offset.to_string()), ("limit", limit.to_string())])
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_series(&self) -> Result<Vec<SeriesSummary>> {
let resp = self
.client
.get(self.url("/books/series"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn list_book_authors(&self) -> Result<Vec<AuthorSummary>> {
let resp = self
.client
.get(self.url("/books/authors"))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn get_reading_progress(
&self,
media_id: &str,
) -> Result<ReadingProgressResponse> {
let resp = self
.client
.get(self.url(&format!("/books/{media_id}/progress")))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(resp)
}
pub async fn update_reading_progress(
&self,
media_id: &str,
current_page: i32,
) -> Result<()> {
self
.client
.put(self.url(&format!("/books/{media_id}/progress")))
.json(&serde_json::json!({"current_page": current_page}))
.send()
.await?
.error_for_status()?;
Ok(())
}
}