little-hesinde/little-hesinde/src/api/opds/search.rs

115 lines
3.2 KiB
Rust

use std::sync::Arc;
use axum::{
extract::{Query, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
};
use serde::Deserialize;
use snafu::{ResultExt, Snafu};
use super::books::{RenderError, render_books};
use crate::{
APP_NAME,
api::{
OPDS_TAG,
error::{ErrorResponse, HttpStatus},
search::{self, SearchQueryError},
},
app_state::AppState,
http_error,
opds::{
error::AsXmlError,
search::{OpenSearchDescription, Url},
},
};
#[derive(Debug, Snafu)]
/// Errors that can occur when searching for books.
pub enum SearchError {
/// A failure to query for books.
#[snafu(display("Failed to query books."))]
Query { source: SearchQueryError },
/// A failure to render the feed.
#[snafu(display("Failed to render feed."))]
Render { source: RenderError },
}
impl HttpStatus for SearchError {
fn status_code(&self) -> StatusCode {
match self {
SearchError::Query { source: _ } => StatusCode::INTERNAL_SERVER_ERROR,
SearchError::Render { source: _ } => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
http_error!(SearchError);
#[derive(Deserialize)]
/// Parameters for a search request.
pub struct Params {
/// Query for a search request.
query: String,
}
/// Search for books and return results as an OPDS feed.
#[utoipa::path(
get,
path = "/search",
tag = OPDS_TAG,
responses(
(status = 200, content_type = "application/atom+xml"),
(status = 500, description = "Error retrieving books from database", body = ErrorResponse)
)
)]
pub async fn handler(
Query(params): Query<Params>,
State(state): State<Arc<AppState>>,
) -> Result<Response, SearchError> {
let books = search::query(&params.query, &state.calibre, &state.config.library_path)
.await
.context(QuerySnafu)?;
render_books(books).await.context(RenderSnafu)
}
#[derive(Debug, Snafu)]
/// Errors that can occur when retrieving search information.
pub enum InfoError {
/// A failure to render the feed.
#[snafu(display("Failed to render feed."))]
FeedRender { source: AsXmlError },
}
impl HttpStatus for InfoError {
fn status_code(&self) -> StatusCode {
match self {
InfoError::FeedRender { source: _ } => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
http_error!(InfoError);
/// Render search information as an OPDS feed.
#[utoipa::path(
get,
path = "/search/info",
tag = OPDS_TAG,
responses(
(status = 200, content_type = "application/atom+xml"),
(status = 500, description = "Internal error", body = ErrorResponse)
)
)]
pub async fn info() -> Result<Response, InfoError> {
let search = OpenSearchDescription {
short_name: APP_NAME.to_string(),
description: "Search for ebooks".to_string(),
input_encoding: "UTF-8".to_string(),
output_encoding: "UTF-8".to_string(),
url: Url {
type_name: "application/atom+xml".to_string(),
template: "/opds/search?query={searchTerms}".to_string(),
},
};
let xml = search.as_xml().context(FeedRenderSnafu)?;
Ok(([(header::CONTENT_TYPE, "application/atom+xml")], xml).into_response())
}