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 58342 additions and 54241 deletions
|
|
@ -1,305 +1,332 @@
|
|||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Extension, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, put},
|
||||
Json,
|
||||
Router,
|
||||
extract::{Extension, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, put},
|
||||
};
|
||||
use pinakes_core::{
|
||||
error::PinakesError,
|
||||
model::{
|
||||
AuthorInfo,
|
||||
BookMetadata,
|
||||
MediaId,
|
||||
Pagination,
|
||||
ReadingProgress,
|
||||
ReadingStatus,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use pinakes_core::{
|
||||
error::PinakesError,
|
||||
model::{AuthorInfo, BookMetadata, MediaId, Pagination, ReadingProgress, ReadingStatus},
|
||||
use crate::{
|
||||
auth::resolve_user_id,
|
||||
dto::MediaResponse,
|
||||
error::ApiError,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
use crate::{auth::resolve_user_id, dto::MediaResponse, error::ApiError, state::AppState};
|
||||
|
||||
/// Book metadata response DTO
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BookMetadataResponse {
|
||||
pub media_id: Uuid,
|
||||
pub isbn: Option<String>,
|
||||
pub isbn13: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub page_count: Option<i32>,
|
||||
pub publication_date: Option<String>,
|
||||
pub series_name: Option<String>,
|
||||
pub series_index: Option<f64>,
|
||||
pub format: Option<String>,
|
||||
pub authors: Vec<AuthorResponse>,
|
||||
pub identifiers: std::collections::HashMap<String, Vec<String>>,
|
||||
pub media_id: Uuid,
|
||||
pub isbn: Option<String>,
|
||||
pub isbn13: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub language: Option<String>,
|
||||
pub page_count: Option<i32>,
|
||||
pub publication_date: Option<String>,
|
||||
pub series_name: Option<String>,
|
||||
pub series_index: Option<f64>,
|
||||
pub format: Option<String>,
|
||||
pub authors: Vec<AuthorResponse>,
|
||||
pub identifiers: std::collections::HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<BookMetadata> for BookMetadataResponse {
|
||||
fn from(meta: BookMetadata) -> Self {
|
||||
Self {
|
||||
media_id: meta.media_id.0,
|
||||
isbn: meta.isbn,
|
||||
isbn13: meta.isbn13,
|
||||
publisher: meta.publisher,
|
||||
language: meta.language,
|
||||
page_count: meta.page_count,
|
||||
publication_date: meta.publication_date.map(|d| d.to_string()),
|
||||
series_name: meta.series_name,
|
||||
series_index: meta.series_index,
|
||||
format: meta.format,
|
||||
authors: meta.authors.into_iter().map(AuthorResponse::from).collect(),
|
||||
identifiers: meta.identifiers,
|
||||
}
|
||||
fn from(meta: BookMetadata) -> Self {
|
||||
Self {
|
||||
media_id: meta.media_id.0,
|
||||
isbn: meta.isbn,
|
||||
isbn13: meta.isbn13,
|
||||
publisher: meta.publisher,
|
||||
language: meta.language,
|
||||
page_count: meta.page_count,
|
||||
publication_date: meta.publication_date.map(|d| d.to_string()),
|
||||
series_name: meta.series_name,
|
||||
series_index: meta.series_index,
|
||||
format: meta.format,
|
||||
authors: meta
|
||||
.authors
|
||||
.into_iter()
|
||||
.map(AuthorResponse::from)
|
||||
.collect(),
|
||||
identifiers: meta.identifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Author response DTO
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthorResponse {
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub file_as: Option<String>,
|
||||
pub position: i32,
|
||||
pub name: String,
|
||||
pub role: String,
|
||||
pub file_as: Option<String>,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
impl From<AuthorInfo> for AuthorResponse {
|
||||
fn from(author: AuthorInfo) -> Self {
|
||||
Self {
|
||||
name: author.name,
|
||||
role: author.role,
|
||||
file_as: author.file_as,
|
||||
position: author.position,
|
||||
}
|
||||
fn from(author: AuthorInfo) -> Self {
|
||||
Self {
|
||||
name: author.name,
|
||||
role: author.role,
|
||||
file_as: author.file_as,
|
||||
position: author.position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reading progress response DTO
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ReadingProgressResponse {
|
||||
pub media_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub current_page: i32,
|
||||
pub total_pages: Option<i32>,
|
||||
pub progress_percent: f64,
|
||||
pub last_read_at: String,
|
||||
pub media_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub current_page: i32,
|
||||
pub total_pages: Option<i32>,
|
||||
pub progress_percent: f64,
|
||||
pub last_read_at: String,
|
||||
}
|
||||
|
||||
impl From<ReadingProgress> for ReadingProgressResponse {
|
||||
fn from(progress: ReadingProgress) -> Self {
|
||||
Self {
|
||||
media_id: progress.media_id.0,
|
||||
user_id: progress.user_id,
|
||||
current_page: progress.current_page,
|
||||
total_pages: progress.total_pages,
|
||||
progress_percent: progress.progress_percent,
|
||||
last_read_at: progress.last_read_at.to_rfc3339(),
|
||||
}
|
||||
fn from(progress: ReadingProgress) -> Self {
|
||||
Self {
|
||||
media_id: progress.media_id.0,
|
||||
user_id: progress.user_id,
|
||||
current_page: progress.current_page,
|
||||
total_pages: progress.total_pages,
|
||||
progress_percent: progress.progress_percent,
|
||||
last_read_at: progress.last_read_at.to_rfc3339(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update reading progress request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateProgressRequest {
|
||||
pub current_page: i32,
|
||||
pub current_page: i32,
|
||||
}
|
||||
|
||||
/// Search books query parameters
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchBooksQuery {
|
||||
pub isbn: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub series: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub language: Option<String>,
|
||||
#[serde(default = "default_offset")]
|
||||
pub offset: u64,
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: u64,
|
||||
pub isbn: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub series: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub language: Option<String>,
|
||||
#[serde(default = "default_offset")]
|
||||
pub offset: u64,
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: u64,
|
||||
}
|
||||
|
||||
fn default_offset() -> u64 {
|
||||
0
|
||||
0
|
||||
}
|
||||
|
||||
fn default_limit() -> u64 {
|
||||
50
|
||||
50
|
||||
}
|
||||
|
||||
/// Series summary DTO
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SeriesSummary {
|
||||
pub name: String,
|
||||
pub book_count: u64,
|
||||
pub name: String,
|
||||
pub book_count: u64,
|
||||
}
|
||||
|
||||
/// Author summary DTO
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthorSummary {
|
||||
pub name: String,
|
||||
pub book_count: u64,
|
||||
pub name: String,
|
||||
pub book_count: u64,
|
||||
}
|
||||
|
||||
/// Get book metadata by media ID
|
||||
pub async fn get_book_metadata(
|
||||
State(state): State<AppState>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
State(state): State<AppState>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let media_id = MediaId(media_id);
|
||||
let metadata = state
|
||||
.storage
|
||||
.get_book_metadata(media_id)
|
||||
.await?
|
||||
.ok_or(ApiError(PinakesError::NotFound(
|
||||
"Book metadata not found".to_string(),
|
||||
)))?;
|
||||
let media_id = MediaId(media_id);
|
||||
let metadata =
|
||||
state
|
||||
.storage
|
||||
.get_book_metadata(media_id)
|
||||
.await?
|
||||
.ok_or(ApiError(PinakesError::NotFound(
|
||||
"Book metadata not found".to_string(),
|
||||
)))?;
|
||||
|
||||
Ok(Json(BookMetadataResponse::from(metadata)))
|
||||
Ok(Json(BookMetadataResponse::from(metadata)))
|
||||
}
|
||||
|
||||
/// List all books with optional search filters
|
||||
pub async fn list_books(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<SearchBooksQuery>,
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<SearchBooksQuery>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let pagination = Pagination {
|
||||
offset: query.offset,
|
||||
limit: query.limit,
|
||||
sort: None,
|
||||
};
|
||||
let pagination = Pagination {
|
||||
offset: query.offset,
|
||||
limit: query.limit,
|
||||
sort: None,
|
||||
};
|
||||
|
||||
let items = state
|
||||
.storage
|
||||
.search_books(
|
||||
query.isbn.as_deref(),
|
||||
query.author.as_deref(),
|
||||
query.series.as_deref(),
|
||||
query.publisher.as_deref(),
|
||||
query.language.as_deref(),
|
||||
&pagination,
|
||||
)
|
||||
.await?;
|
||||
let items = state
|
||||
.storage
|
||||
.search_books(
|
||||
query.isbn.as_deref(),
|
||||
query.author.as_deref(),
|
||||
query.series.as_deref(),
|
||||
query.publisher.as_deref(),
|
||||
query.language.as_deref(),
|
||||
&pagination,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response: Vec<MediaResponse> = items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
let response: Vec<MediaResponse> =
|
||||
items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// List all series with book counts
|
||||
pub async fn list_series(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
|
||||
let series = state.storage.list_series().await?;
|
||||
let response: Vec<SeriesSummary> = series
|
||||
.into_iter()
|
||||
.map(|(name, count)| SeriesSummary {
|
||||
name,
|
||||
book_count: count,
|
||||
})
|
||||
.collect();
|
||||
pub async fn list_series(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let series = state.storage.list_series().await?;
|
||||
let response: Vec<SeriesSummary> = series
|
||||
.into_iter()
|
||||
.map(|(name, count)| {
|
||||
SeriesSummary {
|
||||
name,
|
||||
book_count: count,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(response))
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// Get books in a specific series
|
||||
pub async fn get_series_books(
|
||||
State(state): State<AppState>,
|
||||
Path(series_name): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
Path(series_name): Path<String>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let items = state.storage.get_series_books(&series_name).await?;
|
||||
let response: Vec<MediaResponse> = items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
let items = state.storage.get_series_books(&series_name).await?;
|
||||
let response: Vec<MediaResponse> =
|
||||
items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// List all authors with book counts
|
||||
pub async fn list_authors(
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let authors = state.storage.list_all_authors(&pagination).await?;
|
||||
let response: Vec<AuthorSummary> = authors
|
||||
.into_iter()
|
||||
.map(|(name, count)| AuthorSummary {
|
||||
name,
|
||||
book_count: count,
|
||||
})
|
||||
.collect();
|
||||
let authors = state.storage.list_all_authors(&pagination).await?;
|
||||
let response: Vec<AuthorSummary> = authors
|
||||
.into_iter()
|
||||
.map(|(name, count)| {
|
||||
AuthorSummary {
|
||||
name,
|
||||
book_count: count,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(response))
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// Get books by a specific author
|
||||
pub async fn get_author_books(
|
||||
State(state): State<AppState>,
|
||||
Path(author_name): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
State(state): State<AppState>,
|
||||
Path(author_name): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let items = state
|
||||
.storage
|
||||
.search_books(None, Some(&author_name), None, None, None, &pagination)
|
||||
.await?;
|
||||
let items = state
|
||||
.storage
|
||||
.search_books(None, Some(&author_name), None, None, None, &pagination)
|
||||
.await?;
|
||||
|
||||
let response: Vec<MediaResponse> = items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
let response: Vec<MediaResponse> =
|
||||
items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// Get reading progress for a book
|
||||
pub async fn get_reading_progress(
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
let media_id = MediaId(media_id);
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
let media_id = MediaId(media_id);
|
||||
|
||||
let progress = state
|
||||
.storage
|
||||
.get_reading_progress(user_id.0, media_id)
|
||||
.await?
|
||||
.ok_or(ApiError(PinakesError::NotFound(
|
||||
"Reading progress not found".to_string(),
|
||||
)))?;
|
||||
let progress = state
|
||||
.storage
|
||||
.get_reading_progress(user_id.0, media_id)
|
||||
.await?
|
||||
.ok_or(ApiError(PinakesError::NotFound(
|
||||
"Reading progress not found".to_string(),
|
||||
)))?;
|
||||
|
||||
Ok(Json(ReadingProgressResponse::from(progress)))
|
||||
Ok(Json(ReadingProgressResponse::from(progress)))
|
||||
}
|
||||
|
||||
/// Update reading progress for a book
|
||||
pub async fn update_reading_progress(
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateProgressRequest>,
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Path(media_id): Path<Uuid>,
|
||||
Json(req): Json<UpdateProgressRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
let media_id = MediaId(media_id);
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
let media_id = MediaId(media_id);
|
||||
|
||||
state
|
||||
.storage
|
||||
.update_reading_progress(user_id.0, media_id, req.current_page)
|
||||
.await?;
|
||||
state
|
||||
.storage
|
||||
.update_reading_progress(user_id.0, media_id, req.current_page)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// Get user's reading list
|
||||
pub async fn get_reading_list(
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Query(params): Query<ReadingListQuery>,
|
||||
State(state): State<AppState>,
|
||||
Extension(username): Extension<String>,
|
||||
Query(params): Query<ReadingListQuery>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
let user_id = resolve_user_id(&state.storage, &username).await?;
|
||||
|
||||
let items = state
|
||||
.storage
|
||||
.get_reading_list(user_id.0, params.status)
|
||||
.await?;
|
||||
let items = state
|
||||
.storage
|
||||
.get_reading_list(user_id.0, params.status)
|
||||
.await?;
|
||||
|
||||
let response: Vec<MediaResponse> = items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
let response: Vec<MediaResponse> =
|
||||
items.into_iter().map(MediaResponse::from).collect();
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReadingListQuery {
|
||||
pub status: Option<ReadingStatus>,
|
||||
pub status: Option<ReadingStatus>,
|
||||
}
|
||||
|
||||
/// Build the books router
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
Router::new()
|
||||
// Metadata routes
|
||||
.route("/{id}/metadata", get(get_book_metadata))
|
||||
// Browse routes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue