treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
parent
764aafa88d
commit
3ccddce7fd
178 changed files with 58285 additions and 54241 deletions
|
|
@ -4,285 +4,284 @@ use crate::error::{PinakesError, Result};
|
|||
|
||||
/// OpenLibrary API client for book metadata enrichment
|
||||
pub struct OpenLibraryClient {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl Default for OpenLibraryClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenLibraryClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::builder()
|
||||
.user_agent("Pinakes/1.0")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.expect("Failed to build HTTP client"),
|
||||
base_url: "https://openlibrary.org".to_string(),
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::builder()
|
||||
.user_agent("Pinakes/1.0")
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.expect("Failed to build HTTP client"),
|
||||
base_url: "https://openlibrary.org".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch book metadata by ISBN
|
||||
pub async fn fetch_by_isbn(&self, isbn: &str) -> Result<OpenLibraryBook> {
|
||||
let url = format!("{}/isbn/{}.json", self.base_url, isbn);
|
||||
|
||||
let response = self.client.get(&url).send().await.map_err(|e| {
|
||||
PinakesError::External(format!("OpenLibrary request failed: {}", e))
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"OpenLibrary returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
/// Fetch book metadata by ISBN
|
||||
pub async fn fetch_by_isbn(&self, isbn: &str) -> Result<OpenLibraryBook> {
|
||||
let url = format!("{}/isbn/{}.json", self.base_url, isbn);
|
||||
response.json::<OpenLibraryBook>().await.map_err(|e| {
|
||||
PinakesError::External(format!(
|
||||
"Failed to parse OpenLibrary response: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
let response =
|
||||
self.client.get(&url).send().await.map_err(|e| {
|
||||
PinakesError::External(format!("OpenLibrary request failed: {}", e))
|
||||
})?;
|
||||
/// Search for books by title and author
|
||||
pub async fn search(
|
||||
&self,
|
||||
title: &str,
|
||||
author: Option<&str>,
|
||||
) -> Result<Vec<OpenLibrarySearchResult>> {
|
||||
let mut url = format!(
|
||||
"{}/search.json?title={}",
|
||||
self.base_url,
|
||||
urlencoding::encode(title)
|
||||
);
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"OpenLibrary returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response.json::<OpenLibraryBook>().await.map_err(|e| {
|
||||
PinakesError::External(format!("Failed to parse OpenLibrary response: {}", e))
|
||||
})
|
||||
if let Some(author) = author {
|
||||
url.push_str(&format!("&author={}", urlencoding::encode(author)));
|
||||
}
|
||||
|
||||
/// Search for books by title and author
|
||||
pub async fn search(
|
||||
&self,
|
||||
title: &str,
|
||||
author: Option<&str>,
|
||||
) -> Result<Vec<OpenLibrarySearchResult>> {
|
||||
let mut url = format!(
|
||||
"{}/search.json?title={}",
|
||||
self.base_url,
|
||||
urlencoding::encode(title)
|
||||
);
|
||||
url.push_str("&limit=5");
|
||||
|
||||
if let Some(author) = author {
|
||||
url.push_str(&format!("&author={}", urlencoding::encode(author)));
|
||||
}
|
||||
let response = self.client.get(&url).send().await.map_err(|e| {
|
||||
PinakesError::External(format!("OpenLibrary search failed: {}", e))
|
||||
})?;
|
||||
|
||||
url.push_str("&limit=5");
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PinakesError::External(format!("OpenLibrary search failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"OpenLibrary search returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let search_response: OpenLibrarySearchResponse = response.json().await.map_err(|e| {
|
||||
PinakesError::External(format!("Failed to parse search results: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(search_response.docs)
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"OpenLibrary search returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
/// Fetch cover image by cover ID
|
||||
pub async fn fetch_cover(&self, cover_id: i64, size: CoverSize) -> Result<Vec<u8>> {
|
||||
let size_str = match size {
|
||||
CoverSize::Small => "S",
|
||||
CoverSize::Medium => "M",
|
||||
CoverSize::Large => "L",
|
||||
};
|
||||
let search_response: OpenLibrarySearchResponse =
|
||||
response.json().await.map_err(|e| {
|
||||
PinakesError::External(format!("Failed to parse search results: {}", e))
|
||||
})?;
|
||||
|
||||
let url = format!(
|
||||
"https://covers.openlibrary.org/b/id/{}-{}.jpg",
|
||||
cover_id, size_str
|
||||
);
|
||||
Ok(search_response.docs)
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PinakesError::External(format!("Cover download failed: {}", e)))?;
|
||||
/// Fetch cover image by cover ID
|
||||
pub async fn fetch_cover(
|
||||
&self,
|
||||
cover_id: i64,
|
||||
size: CoverSize,
|
||||
) -> Result<Vec<u8>> {
|
||||
let size_str = match size {
|
||||
CoverSize::Small => "S",
|
||||
CoverSize::Medium => "M",
|
||||
CoverSize::Large => "L",
|
||||
};
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"Cover download returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
let url = format!(
|
||||
"https://covers.openlibrary.org/b/id/{}-{}.jpg",
|
||||
cover_id, size_str
|
||||
);
|
||||
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map(|b| b.to_vec())
|
||||
.map_err(|e| PinakesError::External(format!("Failed to read cover data: {}", e)))
|
||||
let response = self.client.get(&url).send().await.map_err(|e| {
|
||||
PinakesError::External(format!("Cover download failed: {}", e))
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"Cover download returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
/// Fetch cover by ISBN
|
||||
pub async fn fetch_cover_by_isbn(&self, isbn: &str, size: CoverSize) -> Result<Vec<u8>> {
|
||||
let size_str = match size {
|
||||
CoverSize::Small => "S",
|
||||
CoverSize::Medium => "M",
|
||||
CoverSize::Large => "L",
|
||||
};
|
||||
response.bytes().await.map(|b| b.to_vec()).map_err(|e| {
|
||||
PinakesError::External(format!("Failed to read cover data: {}", e))
|
||||
})
|
||||
}
|
||||
|
||||
let url = format!(
|
||||
"https://covers.openlibrary.org/b/isbn/{}-{}.jpg",
|
||||
isbn, size_str
|
||||
);
|
||||
/// Fetch cover by ISBN
|
||||
pub async fn fetch_cover_by_isbn(
|
||||
&self,
|
||||
isbn: &str,
|
||||
size: CoverSize,
|
||||
) -> Result<Vec<u8>> {
|
||||
let size_str = match size {
|
||||
CoverSize::Small => "S",
|
||||
CoverSize::Medium => "M",
|
||||
CoverSize::Large => "L",
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| PinakesError::External(format!("Cover download failed: {}", e)))?;
|
||||
let url = format!(
|
||||
"https://covers.openlibrary.org/b/isbn/{}-{}.jpg",
|
||||
isbn, size_str
|
||||
);
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"Cover download returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
let response = self.client.get(&url).send().await.map_err(|e| {
|
||||
PinakesError::External(format!("Cover download failed: {}", e))
|
||||
})?;
|
||||
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map(|b| b.to_vec())
|
||||
.map_err(|e| PinakesError::External(format!("Failed to read cover data: {}", e)))
|
||||
if !response.status().is_success() {
|
||||
return Err(PinakesError::External(format!(
|
||||
"Cover download returned status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
response.bytes().await.map(|b| b.to_vec()).map_err(|e| {
|
||||
PinakesError::External(format!("Failed to read cover data: {}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CoverSize {
|
||||
Small, // 256x256
|
||||
Medium, // 600x800
|
||||
Large, // Original
|
||||
Small, // 256x256
|
||||
Medium, // 600x800
|
||||
Large, // Original
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenLibraryBook {
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub subtitle: Option<String>,
|
||||
#[serde(default)]
|
||||
pub subtitle: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub authors: Vec<AuthorRef>,
|
||||
#[serde(default)]
|
||||
pub authors: Vec<AuthorRef>,
|
||||
|
||||
#[serde(default)]
|
||||
pub publishers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub publishers: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub publish_date: Option<String>,
|
||||
#[serde(default)]
|
||||
pub publish_date: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub number_of_pages: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub number_of_pages: Option<i32>,
|
||||
|
||||
#[serde(default)]
|
||||
pub subjects: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub subjects: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub covers: Vec<i64>,
|
||||
#[serde(default)]
|
||||
pub covers: Vec<i64>,
|
||||
|
||||
#[serde(default)]
|
||||
pub isbn_10: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub isbn_10: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub isbn_13: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub isbn_13: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub series: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub series: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub description: Option<StringOrObject>,
|
||||
#[serde(default)]
|
||||
pub description: Option<StringOrObject>,
|
||||
|
||||
#[serde(default)]
|
||||
pub languages: Vec<LanguageRef>,
|
||||
#[serde(default)]
|
||||
pub languages: Vec<LanguageRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthorRef {
|
||||
pub key: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageRef {
|
||||
pub key: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum StringOrObject {
|
||||
String(String),
|
||||
Object { value: String },
|
||||
String(String),
|
||||
Object { value: String },
|
||||
}
|
||||
|
||||
impl StringOrObject {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::String(s) => s,
|
||||
Self::Object { value } => value,
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::String(s) => s,
|
||||
Self::Object { value } => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenLibrarySearchResponse {
|
||||
#[serde(default)]
|
||||
pub docs: Vec<OpenLibrarySearchResult>,
|
||||
#[serde(default)]
|
||||
pub docs: Vec<OpenLibrarySearchResult>,
|
||||
|
||||
#[serde(default)]
|
||||
pub num_found: i32,
|
||||
#[serde(default)]
|
||||
pub num_found: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenLibrarySearchResult {
|
||||
#[serde(default)]
|
||||
pub key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub key: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub author_name: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub author_name: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub first_publish_year: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub first_publish_year: Option<i32>,
|
||||
|
||||
#[serde(default)]
|
||||
pub publisher: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub publisher: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub isbn: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub isbn: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub cover_i: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub cover_i: Option<i64>,
|
||||
|
||||
#[serde(default)]
|
||||
pub subject: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub subject: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_openlibrary_client_creation() {
|
||||
let client = OpenLibraryClient::new();
|
||||
assert_eq!(client.base_url, "https://openlibrary.org");
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn test_openlibrary_client_creation() {
|
||||
let client = OpenLibraryClient::new();
|
||||
assert_eq!(client.base_url, "https://openlibrary.org");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_or_object_parsing() {
|
||||
let string_desc: StringOrObject = serde_json::from_str(r#""Simple description""#).unwrap();
|
||||
assert_eq!(string_desc.as_str(), "Simple description");
|
||||
#[test]
|
||||
fn test_string_or_object_parsing() {
|
||||
let string_desc: StringOrObject =
|
||||
serde_json::from_str(r#""Simple description""#).unwrap();
|
||||
assert_eq!(string_desc.as_str(), "Simple description");
|
||||
|
||||
let object_desc: StringOrObject =
|
||||
serde_json::from_str(r#"{"value": "Object description"}"#).unwrap();
|
||||
assert_eq!(object_desc.as_str(), "Object description");
|
||||
}
|
||||
let object_desc: StringOrObject =
|
||||
serde_json::from_str(r#"{"value": "Object description"}"#).unwrap();
|
||||
assert_eq!(object_desc.as_str(), "Object description");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue