From a41dcab889e656b00c895d736c402dd54fcef4ec Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Thu, 9 May 2024 14:24:45 +0200 Subject: [PATCH 01/10] refactor opds to something usable --- rusty-library/src/handlers/author.rs | 25 +- rusty-library/src/handlers/authors.rs | 33 +- rusty-library/src/handlers/books.rs | 40 +- rusty-library/src/handlers/html/author.rs | 18 + rusty-library/src/handlers/html/authors.rs | 18 + rusty-library/src/handlers/html/books.rs | 23 ++ rusty-library/src/handlers/html/recent.rs | 17 + rusty-library/src/handlers/html/series.rs | 18 + .../src/handlers/html/series_single.rs | 18 + rusty-library/src/handlers/opds.rs | 363 ------------------ rusty-library/src/handlers/opds/author.rs | 35 ++ rusty-library/src/handlers/opds/authors.rs | 45 +++ rusty-library/src/handlers/opds/books.rs | 39 ++ rusty-library/src/handlers/opds/feed.rs | 106 +++++ rusty-library/src/handlers/opds/recent.rs | 34 ++ rusty-library/src/handlers/opds/series.rs | 42 ++ .../src/handlers/opds/series_single.rs | 35 ++ rusty-library/src/handlers/paginated.rs | 9 +- rusty-library/src/handlers/recent.rs | 25 ++ rusty-library/src/handlers/recents.rs | 33 -- rusty-library/src/handlers/series.rs | 39 +- rusty-library/src/handlers/series_single.rs | 25 +- rusty-library/src/lib.rs | 55 ++- rusty-library/src/opds/entry.rs | 1 + rusty-library/src/opds/feed.rs | 41 +- 25 files changed, 636 insertions(+), 501 deletions(-) create mode 100644 rusty-library/src/handlers/html/author.rs create mode 100644 rusty-library/src/handlers/html/authors.rs create mode 100644 rusty-library/src/handlers/html/books.rs create mode 100644 rusty-library/src/handlers/html/recent.rs create mode 100644 rusty-library/src/handlers/html/series.rs create mode 100644 rusty-library/src/handlers/html/series_single.rs delete mode 100644 rusty-library/src/handlers/opds.rs create mode 100644 rusty-library/src/handlers/opds/author.rs create mode 100644 rusty-library/src/handlers/opds/authors.rs create mode 100644 rusty-library/src/handlers/opds/books.rs create mode 100644 rusty-library/src/handlers/opds/feed.rs create mode 100644 rusty-library/src/handlers/opds/recent.rs create mode 100644 rusty-library/src/handlers/opds/series.rs create mode 100644 rusty-library/src/handlers/opds/series_single.rs create mode 100644 rusty-library/src/handlers/recent.rs delete mode 100644 rusty-library/src/handlers/recents.rs diff --git a/rusty-library/src/handlers/author.rs b/rusty-library/src/handlers/author.rs index aee053b..5617182 100644 --- a/rusty-library/src/handlers/author.rs +++ b/rusty-library/src/handlers/author.rs @@ -2,21 +2,19 @@ use std::sync::Arc; use calibre_db::data::pagination::SortOrder; use poem::{ - error::InternalServerError, handler, - web::{Data, Html, Path}, + web::{Data, Path}, + Response, }; -use tera::Context; -use crate::{ - app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES, -}; +use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; #[handler] pub async fn handler( id: Path, + accept: Data<&Accept>, state: Data<&Arc>, -) -> Result, poem::Error> { +) -> Result { let author = state .calibre .scalar_author(*id) @@ -30,13 +28,8 @@ pub async fn handler( .filter_map(|x| Book::full_book(x, &state)) .collect::>(); - let mut context = Context::new(); - context.insert("title", &author.name); - context.insert("nav", "authors"); - context.insert("books", &books); - - TEMPLATES - .render("book_list", &context) - .map_err(InternalServerError) - .map(Html) + match accept.0 { + Accept::Html => crate::handlers::html::author::handler(author, books).await, + Accept::Opds => crate::handlers::opds::author::handler(author, books).await, + } } diff --git a/rusty-library/src/handlers/authors.rs b/rusty-library/src/handlers/authors.rs index f85ff2b..28f9e19 100644 --- a/rusty-library/src/handlers/authors.rs +++ b/rusty-library/src/handlers/authors.rs @@ -3,34 +3,37 @@ use std::sync::Arc; use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; use poem::{ handler, - web::{Data, Html, Path}, + web::{Data, Path}, + Response, }; -use crate::{app_state::AppState, handlers::paginated}; +use crate::{app_state::AppState, Accept}; #[handler] -pub async fn handler_init(state: Data<&Arc>) -> Result, poem::Error> { - authors(&state.calibre, None, &SortOrder::ASC) +pub async fn handler_init( + accept: Data<&Accept>, + state: Data<&Arc>, +) -> Result { + authors(&accept, &state.calibre, None, &SortOrder::ASC).await } #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, + accept: Data<&Accept>, state: Data<&Arc>, -) -> Result, poem::Error> { - authors(&state.calibre, Some(&cursor), &sort_order) +) -> Result { + authors(&accept, &state.calibre, Some(&cursor), &sort_order).await } -fn authors( +async fn authors( + acccept: &Accept, calibre: &Calibre, cursor: Option<&str>, sort_order: &SortOrder, -) -> Result, poem::Error> { - paginated::render( - "authors", - || calibre.authors(25, cursor, sort_order), - |author| author.sort.clone(), - |cursor| calibre.has_previous_authors(cursor), - |cursor| calibre.has_more_authors(cursor), - ) +) -> Result { + match acccept { + Accept::Html => crate::handlers::html::authors::handler(calibre, cursor, sort_order).await, + Accept::Opds => crate::handlers::opds::authors::handler(calibre, cursor, sort_order).await, + } } diff --git a/rusty-library/src/handlers/books.rs b/rusty-library/src/handlers/books.rs index e181f91..b481a2b 100644 --- a/rusty-library/src/handlers/books.rs +++ b/rusty-library/src/handlers/books.rs @@ -3,41 +3,37 @@ use std::sync::Arc; use calibre_db::data::pagination::SortOrder; use poem::{ handler, - web::{Data, Html, Path}, + web::{Data, Path}, + Response, }; -use crate::{app_state::AppState, data::book::Book}; - -use super::paginated; +use crate::{app_state::AppState, Accept}; #[handler] -pub async fn handler_init(state: Data<&Arc>) -> Result, poem::Error> { - books(&state, None, &SortOrder::ASC) +pub async fn handler_init( + accept: Data<&Accept>, + state: Data<&Arc>, +) -> Result { + books(&accept, &state, None, &SortOrder::ASC).await } #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, + accept: Data<&Accept>, state: Data<&Arc>, -) -> Result, poem::Error> { - books(&state, Some(&cursor), &sort_order) +) -> Result { + books(&accept, &state, Some(&cursor), &sort_order).await } -fn books( +async fn books( + accept: &Accept, state: &Arc, cursor: Option<&str>, sort_order: &SortOrder, -) -> Result, poem::Error> { - paginated::render( - "books", - || { - state - .calibre - .books(25, cursor, sort_order) - .map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect()) - }, - |book| book.sort.clone(), - |cursor| state.calibre.has_previous_books(cursor), - |cursor| state.calibre.has_more_books(cursor), - ) +) -> Result { + match accept { + Accept::Html => crate::handlers::html::books::handler(state, cursor, sort_order).await, + Accept::Opds => crate::handlers::opds::books::handler(state, cursor, sort_order).await, + } } diff --git a/rusty-library/src/handlers/html/author.rs b/rusty-library/src/handlers/html/author.rs new file mode 100644 index 0000000..477bafe --- /dev/null +++ b/rusty-library/src/handlers/html/author.rs @@ -0,0 +1,18 @@ +use calibre_db::data::author::Author; +use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; +use tera::Context; + +use crate::{data::book::Book, templates::TEMPLATES}; + +pub async fn handler(author: Author, books: Vec) -> Result { + let mut context = Context::new(); + context.insert("title", &author.name); + context.insert("nav", "authors"); + context.insert("books", &books); + + Ok(TEMPLATES + .render("book_list", &context) + .map_err(InternalServerError) + .map(Html)? + .into_response()) +} diff --git a/rusty-library/src/handlers/html/authors.rs b/rusty-library/src/handlers/html/authors.rs new file mode 100644 index 0000000..e4d24a7 --- /dev/null +++ b/rusty-library/src/handlers/html/authors.rs @@ -0,0 +1,18 @@ +use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; +use poem::Response; + +use crate::handlers::paginated; + +pub async fn handler( + calibre: &Calibre, + cursor: Option<&str>, + sort_order: &SortOrder, +) -> Result { + paginated::render( + "authors", + || calibre.authors(25, cursor, sort_order), + |author| author.sort.clone(), + |cursor| calibre.has_previous_authors(cursor), + |cursor| calibre.has_more_authors(cursor), + ) +} diff --git a/rusty-library/src/handlers/html/books.rs b/rusty-library/src/handlers/html/books.rs new file mode 100644 index 0000000..0100204 --- /dev/null +++ b/rusty-library/src/handlers/html/books.rs @@ -0,0 +1,23 @@ +use calibre_db::data::pagination::SortOrder; +use poem::Response; + +use crate::{app_state::AppState, data::book::Book, handlers::paginated}; + +pub async fn handler( + state: &AppState, + cursor: Option<&str>, + sort_order: &SortOrder, +) -> Result { + paginated::render( + "books", + || { + state + .calibre + .books(25, cursor, sort_order) + .map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect()) + }, + |book| book.sort.clone(), + |cursor| state.calibre.has_previous_books(cursor), + |cursor| state.calibre.has_more_books(cursor), + ) +} diff --git a/rusty-library/src/handlers/html/recent.rs b/rusty-library/src/handlers/html/recent.rs new file mode 100644 index 0000000..9f15e7b --- /dev/null +++ b/rusty-library/src/handlers/html/recent.rs @@ -0,0 +1,17 @@ +use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; +use tera::Context; + +use crate::{data::book::Book, templates::TEMPLATES}; + +pub async fn handler(recent_books: Vec) -> Result { + let mut context = Context::new(); + context.insert("title", "Recent Books"); + context.insert("nav", "recent"); + context.insert("books", &recent_books); + + Ok(TEMPLATES + .render("book_list", &context) + .map_err(InternalServerError) + .map(Html)? + .into_response()) +} diff --git a/rusty-library/src/handlers/html/series.rs b/rusty-library/src/handlers/html/series.rs new file mode 100644 index 0000000..d0942bb --- /dev/null +++ b/rusty-library/src/handlers/html/series.rs @@ -0,0 +1,18 @@ +use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; +use poem::Response; + +use crate::handlers::paginated; + +pub async fn handler( + calibre: &Calibre, + cursor: Option<&str>, + sort_order: &SortOrder, +) -> Result { + paginated::render( + "series", + || calibre.series(25, cursor, sort_order), + |series| series.sort.clone(), + |cursor| calibre.has_previous_series(cursor), + |cursor| calibre.has_more_series(cursor), + ) +} diff --git a/rusty-library/src/handlers/html/series_single.rs b/rusty-library/src/handlers/html/series_single.rs new file mode 100644 index 0000000..d9c1dc9 --- /dev/null +++ b/rusty-library/src/handlers/html/series_single.rs @@ -0,0 +1,18 @@ +use calibre_db::data::series::Series; +use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; +use tera::Context; + +use crate::{data::book::Book, templates::TEMPLATES}; + +pub async fn handler(series: Series, books: Vec) -> Result { + let mut context = Context::new(); + context.insert("title", &series.name); + context.insert("nav", "series"); + context.insert("books", &books); + + Ok(TEMPLATES + .render("book_list", &context) + .map_err(InternalServerError) + .map(Html)? + .into_response()) +} diff --git a/rusty-library/src/handlers/opds.rs b/rusty-library/src/handlers/opds.rs deleted file mode 100644 index cfbc880..0000000 --- a/rusty-library/src/handlers/opds.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::sync::Arc; - -use calibre_db::data::{author::Author as DbAuthor, pagination::SortOrder}; -use poem::{ - handler, - web::{Data, Path, WithContentType}, - IntoResponse, -}; -use time::OffsetDateTime; - -use crate::{ - app_state::AppState, - data::book::Book, - handlers::error::HandlerError, - opds::{ - author::Author, content::Content, entry::Entry, feed::Feed, link::Link, - media_type::MediaType, relation::Relation, - }, -}; - -fn create_feed( - now: OffsetDateTime, - id: &str, - title: &str, - self_link: Link, - mut additional_links: Vec, - entries: Vec, -) -> Feed { - let author = Author { - name: "Thallian".to_string(), - uri: "https://code.vanwa.ch/shu/rusty-library".to_string(), - email: None, - }; - let mut links = vec![ - Link { - href: "/opds".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Start, - title: Some("Home".to_string()), - count: None, - }, - self_link, - ]; - links.append(&mut additional_links); - - Feed { - title: title.to_string(), - id: id.to_string(), - updated: now, - icon: "favicon.ico".to_string(), - author, - links, - entries, - } -} - -#[handler] -pub async fn recents_handler( - state: Data<&Arc>, -) -> Result, poem::Error> { - let books = state - .calibre - .recent_books(25) - .map_err(HandlerError::DataError)?; - let books = books - .iter() - .filter_map(|x| Book::full_book(x, &state)) - .collect::>(); - - let entries: Vec = books.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: "/opds/recent".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed( - now, - "rusty:recentbooks", - "Recent Books", - self_link, - vec![], - entries, - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn series_single_handler( - id: Path, - state: Data<&Arc>, -) -> Result, poem::Error> { - let series = state - .calibre - .scalar_series(*id) - .map_err(HandlerError::DataError)?; - let books = state - .calibre - .series_books(*id) - .map_err(HandlerError::DataError)?; - let books = books - .iter() - .filter_map(|x| Book::full_book(x, &state)) - .collect::>(); - - let entries: Vec = books.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: format!("/opds/series/{}", *id), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed( - now, - &format!("rusty:series:{}", *id), - &series.name, - self_link, - vec![], - entries, - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn series_handler( - state: Data<&Arc>, -) -> Result, poem::Error> { - let series = state - .calibre - .series(u32::MAX.into(), None, &SortOrder::ASC) - .map_err(HandlerError::DataError)?; - - let entries: Vec = series.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: "/opds/series".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed( - now, - "rusty:series", - "All Series", - self_link, - vec![], - entries, - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn author_handler( - id: Path, - state: Data<&Arc>, -) -> Result, poem::Error> { - let author = state - .calibre - .scalar_author(*id) - .map_err(HandlerError::DataError)?; - let books = state - .calibre - .author_books(*id, u32::MAX.into(), None, SortOrder::ASC) - .map_err(HandlerError::DataError)?; - let books = books - .iter() - .filter_map(|x| Book::full_book(x, &state)) - .collect::>(); - - let entries: Vec = books.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: format!("/opds/authors/{}", author.id), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed( - now, - &format!("rusty:author:{}", author.id), - &author.name, - self_link, - vec![], - entries, - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn authors_handler( - state: Data<&Arc>, -) -> Result, poem::Error> { - let authors: Vec = state - .calibre - .authors(u32::MAX.into(), None, &SortOrder::ASC) - .map_err(HandlerError::DataError)?; - - let entries: Vec = authors.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: "/opds/authors".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed( - now, - "rusty:authors", - "All Authors", - self_link, - vec![], - entries, - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn books_handler( - state: Data<&Arc>, -) -> Result, poem::Error> { - let books: Vec = state - .calibre - .books(u32::MAX.into(), None, &SortOrder::ASC) - .map(|x| { - x.iter() - .filter_map(|y| Book::full_book(y, &state)) - .collect() - }) - .map_err(HandlerError::DataError)?; - - let entries: Vec = books.into_iter().map(Entry::from).collect(); - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: "/opds/books".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let feed = create_feed(now, "rusty:books", "All Books", self_link, vec![], entries); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} - -#[handler] -pub async fn handler() -> Result, poem::Error> { - let now = OffsetDateTime::now_utc(); - - let self_link = Link { - href: "/opds".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Myself, - title: None, - count: None, - }; - let books_entry = Entry { - title: "Books".to_string(), - id: "rusty:books".to_string(), - updated: now, - content: Some(Content { - media_type: MediaType::Text, - content: "Index of all books".to_string(), - }), - author: None, - links: vec![Link { - href: "/opds/books".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Subsection, - title: None, - count: None, - }], - }; - - let authors_entry = Entry { - title: "Authors".to_string(), - id: "rusty:authors".to_string(), - updated: now, - content: Some(Content { - media_type: MediaType::Text, - content: "Index of all authors".to_string(), - }), - author: None, - links: vec![Link { - href: "/opds/authors".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Subsection, - title: None, - count: None, - }], - }; - - let series_entry = Entry { - title: "Series".to_string(), - id: "rusty:series".to_string(), - updated: now, - content: Some(Content { - media_type: MediaType::Text, - content: "Index of all series".to_string(), - }), - author: None, - links: vec![Link { - href: "/opds/series".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Subsection, - title: None, - count: None, - }], - }; - - let recents_entry = Entry { - title: "Recent Additions".to_string(), - id: "rusty:recentbooks".to_string(), - updated: now, - content: Some(Content { - media_type: MediaType::Text, - content: "Recently added books".to_string(), - }), - author: None, - links: vec![Link { - href: "/opds/recent".to_string(), - media_type: MediaType::Navigation, - rel: Relation::Subsection, - title: None, - count: None, - }], - }; - - let feed = create_feed( - now, - "rusty:catalog", - "Rusty-Library", - self_link, - vec![], - vec![authors_entry, series_entry, books_entry, recents_entry], - ); - let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; - - Ok(xml.with_content_type("application/atom+xml")) -} diff --git a/rusty-library/src/handlers/opds/author.rs b/rusty-library/src/handlers/opds/author.rs new file mode 100644 index 0000000..ccd489d --- /dev/null +++ b/rusty-library/src/handlers/opds/author.rs @@ -0,0 +1,35 @@ +use calibre_db::data::author::Author; +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + data::book::Book, + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler(author: Author, books: Vec) -> Result { + let entries: Vec = books.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: format!("/opds/authors/{}", author.id), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create( + now, + &format!("rusty:author:{}", author.id), + &author.name, + self_link, + vec![], + entries, + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/opds/authors.rs b/rusty-library/src/handlers/opds/authors.rs new file mode 100644 index 0000000..bc60380 --- /dev/null +++ b/rusty-library/src/handlers/opds/authors.rs @@ -0,0 +1,45 @@ +use calibre_db::{ + calibre::Calibre, + data::{author::Author as DbAuthor, pagination::SortOrder}, +}; +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler( + calibre: &Calibre, + _cursor: Option<&str>, + _sort_order: &SortOrder, +) -> Result { + let authors: Vec = calibre + .authors(u32::MAX.into(), None, &SortOrder::ASC) + .map_err(HandlerError::DataError)?; + + let entries: Vec = authors.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: "/opds/authors".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create( + now, + "rusty:authors", + "All Authors", + self_link, + vec![], + entries, + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/opds/books.rs b/rusty-library/src/handlers/opds/books.rs new file mode 100644 index 0000000..0c23a8a --- /dev/null +++ b/rusty-library/src/handlers/opds/books.rs @@ -0,0 +1,39 @@ +use calibre_db::data::pagination::SortOrder; +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + app_state::AppState, + data::book::Book, + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler( + state: &AppState, + _cursor: Option<&str>, + _sort_order: &SortOrder, +) -> Result { + let books: Vec = state + .calibre + .books(u32::MAX.into(), None, &SortOrder::ASC) + .map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect()) + .map_err(HandlerError::DataError)?; + + let entries: Vec = books.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: "/opds/books".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create(now, "rusty:books", "All Books", self_link, vec![], entries); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/opds/feed.rs b/rusty-library/src/handlers/opds/feed.rs new file mode 100644 index 0000000..8534818 --- /dev/null +++ b/rusty-library/src/handlers/opds/feed.rs @@ -0,0 +1,106 @@ +use poem::{handler, web::WithContentType, IntoResponse}; +use time::OffsetDateTime; + +use crate::{ + handlers::error::HandlerError, + opds::{ + content::Content, entry::Entry, feed::Feed, link::Link, media_type::MediaType, + relation::Relation, + }, +}; + +#[handler] +pub async fn handler() -> Result, poem::Error> { + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: "/opds".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let books_entry = Entry { + title: "Books".to_string(), + id: "rusty:books".to_string(), + updated: now, + content: Some(Content { + media_type: MediaType::Text, + content: "Index of all books".to_string(), + }), + author: None, + links: vec![Link { + href: "/opds/books".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Subsection, + title: None, + count: None, + }], + }; + + let authors_entry = Entry { + title: "Authors".to_string(), + id: "rusty:authors".to_string(), + updated: now, + content: Some(Content { + media_type: MediaType::Text, + content: "Index of all authors".to_string(), + }), + author: None, + links: vec![Link { + href: "/opds/authors".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Subsection, + title: None, + count: None, + }], + }; + + let series_entry = Entry { + title: "Series".to_string(), + id: "rusty:series".to_string(), + updated: now, + content: Some(Content { + media_type: MediaType::Text, + content: "Index of all series".to_string(), + }), + author: None, + links: vec![Link { + href: "/opds/series".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Subsection, + title: None, + count: None, + }], + }; + + let recents_entry = Entry { + title: "Recent Additions".to_string(), + id: "rusty:recentbooks".to_string(), + updated: now, + content: Some(Content { + media_type: MediaType::Text, + content: "Recently added books".to_string(), + }), + author: None, + links: vec![Link { + href: "/opds/recent".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Subsection, + title: None, + count: None, + }], + }; + + let feed = Feed::create( + now, + "rusty:catalog", + "Rusty-Library", + self_link, + vec![], + vec![authors_entry, series_entry, books_entry, recents_entry], + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml.with_content_type("application/atom+xml")) +} diff --git a/rusty-library/src/handlers/opds/recent.rs b/rusty-library/src/handlers/opds/recent.rs new file mode 100644 index 0000000..d3197fe --- /dev/null +++ b/rusty-library/src/handlers/opds/recent.rs @@ -0,0 +1,34 @@ +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + data::book::Book, + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler(recent_books: Vec) -> Result { + let entries: Vec = recent_books.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: "/opds/recent".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create( + now, + "rusty:recentbooks", + "Recent Books", + self_link, + vec![], + entries, + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/opds/series.rs b/rusty-library/src/handlers/opds/series.rs new file mode 100644 index 0000000..04598f8 --- /dev/null +++ b/rusty-library/src/handlers/opds/series.rs @@ -0,0 +1,42 @@ +use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler( + calibre: &Calibre, + _cursor: Option<&str>, + _sort_order: &SortOrder, +) -> Result { + let series = calibre + .series(u32::MAX.into(), None, &SortOrder::ASC) + .map_err(HandlerError::DataError)?; + + let entries: Vec = series.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: "/opds/series".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create( + now, + "rusty:series", + "All Series", + self_link, + vec![], + entries, + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/opds/series_single.rs b/rusty-library/src/handlers/opds/series_single.rs new file mode 100644 index 0000000..370ff47 --- /dev/null +++ b/rusty-library/src/handlers/opds/series_single.rs @@ -0,0 +1,35 @@ +use calibre_db::data::series::Series; +use poem::{IntoResponse, Response}; +use time::OffsetDateTime; + +use crate::{ + data::book::Book, + handlers::error::HandlerError, + opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, +}; + +pub async fn handler(series: Series, books: Vec) -> Result { + let entries: Vec = books.into_iter().map(Entry::from).collect(); + let now = OffsetDateTime::now_utc(); + + let self_link = Link { + href: format!("/opds/series/{}", series.id), + media_type: MediaType::Navigation, + rel: Relation::Myself, + title: None, + count: None, + }; + let feed = Feed::create( + now, + &format!("rusty:series:{}", series.id), + &series.name, + self_link, + vec![], + entries, + ); + let xml = feed.as_xml().map_err(HandlerError::OpdsError)?; + + Ok(xml + .with_content_type("application/atom+xml") + .into_response()) +} diff --git a/rusty-library/src/handlers/paginated.rs b/rusty-library/src/handlers/paginated.rs index 2b83b43..c024032 100644 --- a/rusty-library/src/handlers/paginated.rs +++ b/rusty-library/src/handlers/paginated.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use calibre_db::data::error::DataStoreError; -use poem::{error::InternalServerError, web::Html}; +use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; use serde::Serialize; use tera::Context; @@ -15,7 +15,7 @@ pub fn render( sort_field: S, has_previous: P, has_more: M, -) -> Result, poem::Error> +) -> Result where F: Fn() -> Result, DataStoreError>, S: Fn(&T) -> String, @@ -42,8 +42,9 @@ where context.insert("nav", template); context.insert(template, &items); - TEMPLATES + Ok(TEMPLATES .render(template, &context) .map_err(InternalServerError) - .map(Html) + .map(Html)? + .into_response()) } diff --git a/rusty-library/src/handlers/recent.rs b/rusty-library/src/handlers/recent.rs new file mode 100644 index 0000000..ec630ef --- /dev/null +++ b/rusty-library/src/handlers/recent.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use poem::{handler, web::Data, Response}; + +use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; + +#[handler] +pub async fn handler( + accept: Data<&Accept>, + state: Data<&Arc>, +) -> Result { + let recent_books = state + .calibre + .recent_books(25) + .map_err(HandlerError::DataError)?; + let recent_books = recent_books + .iter() + .filter_map(|x| Book::full_book(x, &state)) + .collect::>(); + + match accept.0 { + Accept::Html => crate::handlers::html::recent::handler(recent_books).await, + Accept::Opds => crate::handlers::opds::recent::handler(recent_books).await, + } +} diff --git a/rusty-library/src/handlers/recents.rs b/rusty-library/src/handlers/recents.rs deleted file mode 100644 index 1261542..0000000 --- a/rusty-library/src/handlers/recents.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::sync::Arc; - -use poem::{ - error::InternalServerError, - handler, - web::{Data, Html}, -}; -use tera::Context; - -use crate::{ - app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES, -}; - -#[handler] -pub async fn handler(state: Data<&Arc>) -> Result, poem::Error> { - let recent_books = state - .calibre - .recent_books(25) - .map_err(HandlerError::DataError)?; - let recent_books = recent_books - .iter() - .filter_map(|x| Book::full_book(x, &state)) - .collect::>(); - - let mut context = Context::new(); - context.insert("title", "Recent Books"); - context.insert("nav", "recent"); - context.insert("books", &recent_books); - TEMPLATES - .render("book_list", &context) - .map_err(InternalServerError) - .map(Html) -} diff --git a/rusty-library/src/handlers/series.rs b/rusty-library/src/handlers/series.rs index 50cb9e3..e48bcf5 100644 --- a/rusty-library/src/handlers/series.rs +++ b/rusty-library/src/handlers/series.rs @@ -3,36 +3,41 @@ use std::sync::Arc; use calibre_db::data::pagination::SortOrder; use poem::{ handler, - web::{Data, Html, Path}, + web::{Data, Path}, + Response, }; -use crate::app_state::AppState; - -use super::paginated; +use crate::{app_state::AppState, Accept}; #[handler] -pub async fn handler_init(state: Data<&Arc>) -> Result, poem::Error> { - series(&state, None, &SortOrder::ASC) +pub async fn handler_init( + accept: Data<&Accept>, + state: Data<&Arc>, +) -> Result { + series(&accept, &state, None, &SortOrder::ASC).await } #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, + accept: Data<&Accept>, state: Data<&Arc>, -) -> Result, poem::Error> { - series(&state, Some(&cursor), &sort_order) +) -> Result { + series(&accept, &state, Some(&cursor), &sort_order).await } -fn series( +async fn series( + accept: &Accept, state: &Arc, cursor: Option<&str>, sort_order: &SortOrder, -) -> Result, poem::Error> { - paginated::render( - "series", - || state.calibre.series(25, cursor, sort_order), - |series| series.sort.clone(), - |cursor| state.calibre.has_previous_series(cursor), - |cursor| state.calibre.has_more_series(cursor), - ) +) -> Result { + match accept { + Accept::Html => { + crate::handlers::html::series::handler(&state.calibre, cursor, sort_order).await + } + Accept::Opds => { + crate::handlers::opds::series::handler(&state.calibre, cursor, sort_order).await + } + } } diff --git a/rusty-library/src/handlers/series_single.rs b/rusty-library/src/handlers/series_single.rs index 600fd99..1470e79 100644 --- a/rusty-library/src/handlers/series_single.rs +++ b/rusty-library/src/handlers/series_single.rs @@ -1,21 +1,19 @@ use std::sync::Arc; use poem::{ - error::InternalServerError, handler, - web::{Data, Html, Path}, + web::{Data, Path}, + Response, }; -use tera::Context; -use crate::{ - app_state::AppState, data::book::Book, handlers::error::HandlerError, templates::TEMPLATES, -}; +use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; #[handler] pub async fn handler( id: Path, + accept: Data<&Accept>, state: Data<&Arc>, -) -> Result, poem::Error> { +) -> Result { let series = state .calibre .scalar_series(*id) @@ -29,13 +27,8 @@ pub async fn handler( .filter_map(|x| Book::full_book(x, &state)) .collect::>(); - let mut context = Context::new(); - context.insert("title", &series.name); - context.insert("nav", "series"); - context.insert("books", &books); - - TEMPLATES - .render("book_list", &context) - .map_err(InternalServerError) - .map(Html) + match accept.0 { + Accept::Html => crate::handlers::html::series_single::handler(series, books).await, + Accept::Opds => crate::handlers::opds::series_single::handler(series, books).await, + } } diff --git a/rusty-library/src/lib.rs b/rusty-library/src/lib.rs index a8bb2d1..9645fcc 100644 --- a/rusty-library/src/lib.rs +++ b/rusty-library/src/lib.rs @@ -17,21 +17,43 @@ pub mod data { pub mod book; } pub mod handlers { + pub mod html { + pub mod author; + pub mod authors; + pub mod books; + pub mod recent; + pub mod series; + pub mod series_single; + } + pub mod opds { + pub mod author; + pub mod authors; + pub mod books; + pub mod feed; + pub mod recent; + pub mod series; + pub mod series_single; + } pub mod author; pub mod authors; pub mod books; pub mod cover; pub mod download; pub mod error; - pub mod opds; pub mod paginated; - pub mod recents; + pub mod recent; pub mod series; pub mod series_single; } pub mod opds; pub mod templates; +#[derive(Debug, Clone, Copy)] +pub enum Accept { + Html, + Opds, +} + #[derive(RustEmbed)] #[folder = "static"] pub struct Files; @@ -40,18 +62,8 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> { let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database"); let app_state = Arc::new(AppState { calibre, config }); - let app = Route::new() - .at("/", get(handlers::recents::handler)) - .at("/opds", get(handlers::opds::handler)) - .at("/opds/recent", get(handlers::opds::recents_handler)) - .at("/opds/books", get(handlers::opds::books_handler)) - .at("/opds/authors", get(handlers::opds::authors_handler)) - .at("/opds/authors/:id", get(handlers::opds::author_handler)) - .at("/opds/series", get(handlers::opds::series_handler)) - .at( - "/opds/series/:id", - get(handlers::opds::series_single_handler), - ) + let html_routes = Route::new() + .at("/", get(handlers::recent::handler)) .at("/books", get(handlers::books::handler_init)) .at("/books/:cursor/:sort_order", get(handlers::books::handler)) .at("/series", get(handlers::series::handler_init)) @@ -69,6 +81,21 @@ pub async fn run(config: Config) -> Result<(), std::io::Error> { .at("/cover/:id", get(handlers::cover::handler)) .at("/book/:id/:format", get(handlers::download::handler)) .nest("/static", EmbeddedFilesEndpoint::::new()) + .data(Accept::Html); + + let opds_routes = Route::new() + .at("/", get(handlers::opds::feed::handler)) + .at("/recent", get(handlers::recent::handler)) + .at("/books", get(handlers::books::handler_init)) + .at("/authors", get(handlers::authors::handler_init)) + .at("/authors/:id", get(handlers::author::handler)) + .at("/series", get(handlers::series::handler_init)) + .at("/series/:id", get(handlers::series_single::handler)) + .data(Accept::Opds); + + let app = Route::new() + .nest("/", html_routes) + .nest("/opds", opds_routes) .data(app_state) .with(Tracing); diff --git a/rusty-library/src/opds/entry.rs b/rusty-library/src/opds/entry.rs index ef518ed..39f5345 100644 --- a/rusty-library/src/opds/entry.rs +++ b/rusty-library/src/opds/entry.rs @@ -17,6 +17,7 @@ pub struct Entry { pub updated: OffsetDateTime, #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(rename = "link")] pub links: Vec, diff --git a/rusty-library/src/opds/feed.rs b/rusty-library/src/opds/feed.rs index 518d09b..6d02ece 100644 --- a/rusty-library/src/opds/feed.rs +++ b/rusty-library/src/opds/feed.rs @@ -8,7 +8,10 @@ use quick_xml::{ use serde::Serialize; use time::OffsetDateTime; -use super::{author::Author, entry::Entry, error::OpdsError, link::Link}; +use super::{ + author::Author, entry::Entry, error::OpdsError, link::Link, media_type::MediaType, + relation::Relation, +}; #[derive(Debug, Serialize)] #[serde(rename = "feed")] @@ -26,6 +29,42 @@ pub struct Feed { } impl Feed { + pub fn create( + now: OffsetDateTime, + id: &str, + title: &str, + self_link: Link, + mut additional_links: Vec, + entries: Vec, + ) -> Self { + let author = Author { + name: "Thallian".to_string(), + uri: "https://code.vanwa.ch/shu/rusty-library".to_string(), + email: None, + }; + let mut links = vec![ + Link { + href: "/opds".to_string(), + media_type: MediaType::Navigation, + rel: Relation::Start, + title: Some("Home".to_string()), + count: None, + }, + self_link, + ]; + links.append(&mut additional_links); + + Self { + title: title.to_string(), + id: id.to_string(), + updated: now, + icon: "favicon.ico".to_string(), + author, + links, + entries, + } + } + pub fn as_xml(&self) -> Result { let xml = to_string(&self)?; let mut reader = Reader::from_str(&xml); From 870f457f1bce225a2829bd494a48f965d930985d Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Fri, 10 May 2024 14:25:18 +0200 Subject: [PATCH 02/10] bit of documentation --- calibre-db/src/calibre.rs | 28 ++++++++++++ calibre-db/src/data/author.rs | 12 +++++ calibre-db/src/data/book.rs | 21 ++++++++- calibre-db/src/data/error.rs | 9 ++++ calibre-db/src/data/pagination.rs | 16 ++++++- calibre-db/src/data/series.rs | 12 +++++ calibre-db/src/lib.rs | 3 ++ rusty-library/src/app_state.rs | 5 +++ rusty-library/src/basic_auth.rs | 44 ------------------- rusty-library/src/cli.rs | 2 + rusty-library/src/config.rs | 9 ++++ rusty-library/src/data/book.rs | 42 +++++++++++------- rusty-library/src/handlers/author.rs | 3 ++ rusty-library/src/handlers/authors.rs | 5 +++ rusty-library/src/handlers/books.rs | 5 +++ rusty-library/src/handlers/cover.rs | 3 ++ rusty-library/src/handlers/download.rs | 9 +++- rusty-library/src/handlers/error.rs | 9 ++++ rusty-library/src/handlers/html/author.rs | 3 ++ rusty-library/src/handlers/html/authors.rs | 3 ++ rusty-library/src/handlers/html/books.rs | 5 ++- rusty-library/src/handlers/html/recent.rs | 3 ++ rusty-library/src/handlers/html/series.rs | 3 ++ .../src/handlers/html/series_single.rs | 3 ++ rusty-library/src/handlers/opds/author.rs | 3 ++ rusty-library/src/handlers/opds/authors.rs | 3 ++ rusty-library/src/handlers/opds/books.rs | 3 ++ rusty-library/src/handlers/opds/feed.rs | 3 ++ rusty-library/src/handlers/opds/recent.rs | 3 ++ rusty-library/src/handlers/opds/series.rs | 3 ++ .../src/handlers/opds/series_single.rs | 3 ++ rusty-library/src/handlers/paginated.rs | 3 ++ rusty-library/src/handlers/recent.rs | 3 ++ rusty-library/src/handlers/series.rs | 5 +++ rusty-library/src/handlers/series_single.rs | 3 ++ rusty-library/src/lib.rs | 28 +++++++++++- rusty-library/src/opds/author.rs | 6 +++ rusty-library/src/opds/content.rs | 5 +++ rusty-library/src/opds/entry.rs | 29 +++++++++--- rusty-library/src/opds/error.rs | 6 +++ rusty-library/src/opds/feed.rs | 12 +++++ rusty-library/src/opds/link.rs | 11 ++++- rusty-library/src/opds/media_type.rs | 7 +++ rusty-library/src/opds/mod.rs | 8 ---- rusty-library/src/opds/relation.rs | 8 ++++ rusty-library/src/templates.rs | 3 ++ rusty-library/templates/base.html | 6 ++- 47 files changed, 341 insertions(+), 80 deletions(-) delete mode 100644 rusty-library/src/basic_auth.rs delete mode 100644 rusty-library/src/opds/mod.rs diff --git a/calibre-db/src/calibre.rs b/calibre-db/src/calibre.rs index 221a4cb..fc6bc53 100644 --- a/calibre-db/src/calibre.rs +++ b/calibre-db/src/calibre.rs @@ -1,3 +1,5 @@ +//! Bundle all functions together. + use std::path::Path; use r2d2::Pool; @@ -7,12 +9,17 @@ use crate::data::{ author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series, }; +/// Top level calibre functions, bundling all sub functions in one place and providing secure access to +/// the database. #[derive(Debug, Clone)] pub struct Calibre { pool: Pool, } impl Calibre { + /// Open a connection to the calibre database. + /// + /// Fail if the database file can not be opened or not be found. pub fn load(path: &Path) -> Result { let manager = SqliteConnectionManager::file(path); let pool = r2d2::Pool::new(manager)?; @@ -20,6 +27,8 @@ impl Calibre { Ok(Self { pool }) } + /// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn books( &self, limit: u64, @@ -30,6 +39,8 @@ impl Calibre { Book::multiple(&conn, limit, cursor, sort_order) } + /// Fetch author data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn authors( &self, limit: u64, @@ -40,6 +51,8 @@ impl Calibre { Author::multiple(&conn, limit, cursor, sort_order) } + /// Fetch books for an author specified by `author_id`, paginate the books by starting at `cursor`, + /// fetching up to an amount of `limit` and ordering by `sort_order`. pub fn author_books( &self, author_id: u64, @@ -51,21 +64,26 @@ impl Calibre { Book::author_books(&conn, author_id, limit, cursor, sort_order) } + /// Get recent books up to a limit of `limit`. pub fn recent_books(&self, limit: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Book::recents(&conn, limit) } + /// Get a single book, specified `id`. pub fn scalar_book(&self, id: u64) -> Result { let conn = self.pool.get()?; Book::scalar_book(&conn, id) } + /// Get the author to a book with id `id`. pub fn book_author(&self, id: u64) -> Result { let conn = self.pool.get()?; Author::book_author(&conn, id) } + /// Fetch series data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn series( &self, limit: u64, @@ -76,51 +94,61 @@ impl Calibre { Series::multiple(&conn, limit, cursor, sort_order) } + /// Get the series a book with id `id` is in, as well as the book's position within the series. pub fn book_series(&self, id: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Series::book_series(&conn, id) } + /// Get all books belonging to the series with id `id`. pub fn series_books(&self, id: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Book::series_books(&conn, id) } + /// Check if there are more authors before the specified cursor. pub fn has_previous_authors(&self, author_sort: &str) -> Result { let conn = self.pool.get()?; Author::has_previous_authors(&conn, author_sort) } + /// Check if there are more authors after the specified cursor. pub fn has_more_authors(&self, author_sort: &str) -> Result { let conn = self.pool.get()?; Author::has_more_authors(&conn, author_sort) } + /// Check if there are more books before the specified cursor. pub fn has_previous_books(&self, book_sort: &str) -> Result { let conn = self.pool.get()?; Book::has_previous_books(&conn, book_sort) } + /// Check if there are more books after the specified cursor. pub fn has_more_books(&self, book_sort: &str) -> Result { let conn = self.pool.get()?; Book::has_more_books(&conn, book_sort) } + /// Check if there are more series before the specified cursor. pub fn has_previous_series(&self, series_sort: &str) -> Result { let conn = self.pool.get()?; Series::has_previous_series(&conn, series_sort) } + /// Check if there are more series after the specified cursor. pub fn has_more_series(&self, series_sort: &str) -> Result { let conn = self.pool.get()?; Series::has_more_series(&conn, series_sort) } + /// Fetch a single author with id `id`. pub fn scalar_author(&self, id: u64) -> Result { let conn = self.pool.get()?; Author::scalar_author(&conn, id) } + /// Fetch a single series with id `id`. pub fn scalar_series(&self, id: u64) -> Result { let conn = self.pool.get()?; Series::scalar_series(&conn, id) diff --git a/calibre-db/src/data/author.rs b/calibre-db/src/data/author.rs index 2ecf9e5..e79da5a 100644 --- a/calibre-db/src/data/author.rs +++ b/calibre-db/src/data/author.rs @@ -1,3 +1,5 @@ +//! Author data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; @@ -6,10 +8,14 @@ use super::{ pagination::{Pagination, SortOrder}, }; +/// Author in calibre. #[derive(Debug, Clone, Serialize)] pub struct Author { + /// Id in database. pub id: u64, + /// Full name. pub name: String, + /// Full name for sorting. pub sort: String, } @@ -22,6 +28,8 @@ impl Author { }) } + /// Fetch author data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -37,6 +45,7 @@ impl Author { ) } + /// Get the author to a book with id `id`. pub fn book_author(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare( "SELECT authors.id, authors.name, authors.sort FROM authors \ @@ -47,12 +56,14 @@ impl Author { Ok(stmt.query_row(params, Self::from_row)?) } + /// Fetch a single author with id `id`. pub fn scalar_author(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare("SELECT id, name, sort FROM authors WHERE id = (:id)")?; let params = named_params! { ":id": id }; Ok(stmt.query_row(params, Self::from_row)?) } + /// Check if there are more authors before the specified cursor. pub fn has_previous_authors( conn: &Connection, sort_name: &str, @@ -60,6 +71,7 @@ impl Author { Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::DESC) } + /// Check if there are more authors after the specified cursor. pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::ASC) } diff --git a/calibre-db/src/data/book.rs b/calibre-db/src/data/book.rs index a893b2e..4547008 100644 --- a/calibre-db/src/data/book.rs +++ b/calibre-db/src/data/book.rs @@ -1,3 +1,5 @@ +//! Book data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; use time::OffsetDateTime; @@ -7,14 +9,22 @@ use super::{ pagination::{Pagination, SortOrder}, }; -#[derive(Debug, Serialize)] +/// Book in calibre. +#[derive(Debug, Clone, Serialize)] pub struct Book { + /// Id in database. pub id: u64, + /// Book title. pub title: String, + /// Book title for sorting. pub sort: String, + /// Folder of the book within the calibre library. pub path: String, + /// Uuid of the book. pub uuid: String, + /// When was the book last modified. pub last_modified: OffsetDateTime, + /// Optional description. pub description: Option, } @@ -31,6 +41,8 @@ impl Book { }) } + /// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -47,6 +59,8 @@ impl Book { ) } + /// Fetch books for an author specified by `author_id`, paginate the books by starting at `cursor`, + /// fetching up to an amount of `limit` and ordering by `sort_order`. pub fn author_books( conn: &Connection, author_id: u64, @@ -66,6 +80,7 @@ impl Book { ) } + /// Get all books belonging to the series with id `id`. pub fn series_books(conn: &Connection, id: u64) -> Result, DataStoreError> { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text FROM series \ @@ -80,6 +95,7 @@ impl Book { Ok(iter.filter_map(Result::ok).collect()) } + /// Get recent books up to a limit of `limit`. pub fn recents(conn: &Connection, limit: u64) -> Result, DataStoreError> { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \ @@ -90,6 +106,7 @@ impl Book { Ok(iter.filter_map(Result::ok).collect()) } + /// Get a single book, specified `id`. pub fn scalar_book(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \ @@ -99,10 +116,12 @@ impl Book { Ok(stmt.query_row(params, Self::from_row)?) } + /// Check if there are more books before the specified cursor. pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result { Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::DESC) } + /// Check if there are more books after the specified cursor. pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result { Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC) } diff --git a/calibre-db/src/data/error.rs b/calibre-db/src/data/error.rs index 0680abc..ea78a79 100644 --- a/calibre-db/src/data/error.rs +++ b/calibre-db/src/data/error.rs @@ -1,19 +1,28 @@ +//! Error handling for calibre database access. + use thiserror::Error; use time::error::Parse; +/// Errors from accessing the calibre database. #[derive(Error, Debug)] #[error("data store error")] pub enum DataStoreError { + /// Found no entries for the query. #[error("no results")] NoResults(rusqlite::Error), + /// Error with SQLite. #[error("sqlite error")] SqliteError(rusqlite::Error), + /// Error connecting to the database. #[error("connection error")] ConnectionError(#[from] r2d2::Error), + /// Error wparsing a datetime from the database. #[error("failed to parse datetime")] DateTimeError(#[from] Parse), } +/// Convert an SQLite error into a proper NoResults one if the query +/// returned no rows, return others as is. impl From for DataStoreError { fn from(error: rusqlite::Error) -> Self { match error { diff --git a/calibre-db/src/data/pagination.rs b/calibre-db/src/data/pagination.rs index d08449b..c2f6064 100644 --- a/calibre-db/src/data/pagination.rs +++ b/calibre-db/src/data/pagination.rs @@ -1,22 +1,33 @@ +//! Cursor pagination handling. + use rusqlite::{named_params, Connection, Row, ToSql}; use serde::{Deserialize, Serialize}; use super::error::DataStoreError; +/// How to sort query results. Signifying whether we are paginating forwards or backwards. #[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] pub enum SortOrder { + /// Forwards ASC, + /// Backwards DESC, } +/// Pagination data. pub struct Pagination<'a> { + /// Sort by this column. pub sort_col: &'a str, + /// Limit returned results. pub limit: u64, + /// Where to start paginating. pub cursor: Option<&'a str>, + /// Paginating forwards or backwards. pub sort_order: SortOrder, } impl<'a> Pagination<'a> { + /// Create a new pagination. pub fn new( sort_col: &'a str, cursor: Option<&'a str>, @@ -40,6 +51,7 @@ impl<'a> Pagination<'a> { .to_string() } + /// Check if there are more items forwards or backwards from `cursor` (direction specified by `sort_order`). pub fn has_prev_or_more( conn: &Connection, table: &str, @@ -57,6 +69,7 @@ impl<'a> Pagination<'a> { Ok(count > 0) } + /// Paginate a statement. pub fn paginate( &self, conn: &Connection, @@ -77,7 +90,8 @@ impl<'a> Pagination<'a> { }; let sort_col = self.sort_col; - // otherwise paginated statements with join will fail + // otherwise paginated statements with join will fails, not happy with this but fine for + // now let sort_col_wrapped = if let Some(index) = sort_col.find('.') { let right_part = &sort_col[index..]; "t".to_owned() + right_part diff --git a/calibre-db/src/data/series.rs b/calibre-db/src/data/series.rs index ffe43bf..b467fde 100644 --- a/calibre-db/src/data/series.rs +++ b/calibre-db/src/data/series.rs @@ -1,3 +1,5 @@ +//! Series data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; @@ -6,10 +8,14 @@ use super::{ pagination::{Pagination, SortOrder}, }; +/// Series in calibre. #[derive(Debug, Clone, Serialize)] pub struct Series { + /// Id in database. pub id: u64, + /// Series name. pub name: String, + /// Series name for sorting. pub sort: String, } @@ -22,6 +28,8 @@ impl Series { }) } + /// Fetch series data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -37,12 +45,14 @@ impl Series { ) } + /// Fetch a single series with id `id`. pub fn scalar_series(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare("SELECT id, name, sort FROM series WHERE id = (:id)")?; let params = named_params! { ":id": id }; Ok(stmt.query_row(params, Self::from_row)?) } + /// Get the series a book with id `id` is in, as well as the book's position within the series. pub fn book_series( conn: &Connection, book_id: u64, @@ -68,10 +78,12 @@ impl Series { } } + /// Check if there are more series before the specified cursor. pub fn has_previous_series(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::DESC) } + /// Check if there are more series after the specified cursor. pub fn has_more_series(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::ASC) } diff --git a/calibre-db/src/lib.rs b/calibre-db/src/lib.rs index c7a0452..bae8fbc 100644 --- a/calibre-db/src/lib.rs +++ b/calibre-db/src/lib.rs @@ -1,4 +1,7 @@ +//! Read data from a calibre library, leveraging its SQLite metadata database. + pub mod calibre; +/// Data structs for the calibre database. pub mod data { pub mod author; pub mod book; diff --git a/rusty-library/src/app_state.rs b/rusty-library/src/app_state.rs index 9b886cc..7136258 100644 --- a/rusty-library/src/app_state.rs +++ b/rusty-library/src/app_state.rs @@ -1,8 +1,13 @@ +//! Data for global app state. + use calibre_db::calibre::Calibre; use crate::config::Config; +/// Global application state, meant to be used in request handlers. pub struct AppState { + /// Access calibre database. pub calibre: Calibre, + /// Access application configuration. pub config: Config, } diff --git a/rusty-library/src/basic_auth.rs b/rusty-library/src/basic_auth.rs deleted file mode 100644 index 092bde4..0000000 --- a/rusty-library/src/basic_auth.rs +++ /dev/null @@ -1,44 +0,0 @@ -use poem::{ - http::StatusCode, - web::{ - headers, - headers::{authorization::Basic, HeaderMapExt}, - }, - Endpoint, Error, Middleware, Request, Result, -}; - -pub struct BasicAuth { - pub username: String, - pub password: String, -} - -impl Middleware for BasicAuth { - type Output = BasicAuthEndpoint; - - fn transform(&self, ep: E) -> Self::Output { - BasicAuthEndpoint { - ep, - username: self.username.clone(), - password: self.password.clone(), - } - } -} - -pub struct BasicAuthEndpoint { - ep: E, - username: String, - password: String, -} - -impl Endpoint for BasicAuthEndpoint { - type Output = E::Output; - - async fn call(&self, req: Request) -> Result { - if let Some(auth) = req.headers().typed_get::>() { - if auth.0.username() == self.username && auth.0.password() == self.password { - return self.ep.call(req).await; - } - } - Err(Error::from_status(StatusCode::UNAUTHORIZED)) - } -} diff --git a/rusty-library/src/cli.rs b/rusty-library/src/cli.rs index 5a7f28c..6f1c404 100644 --- a/rusty-library/src/cli.rs +++ b/rusty-library/src/cli.rs @@ -1,3 +1,5 @@ +//! Cli interface. + use clap::Parser; /// Simple opds server for calibre diff --git a/rusty-library/src/config.rs b/rusty-library/src/config.rs index 0747cf3..9d0b97e 100644 --- a/rusty-library/src/config.rs +++ b/rusty-library/src/config.rs @@ -1,23 +1,32 @@ +//! Configuration data. + use std::path::{Path, PathBuf}; use thiserror::Error; use crate::cli::Cli; +/// Errors when dealing with application configuration. #[derive(Error, Debug)] pub enum ConfigError { + /// Calibre library path does not exist. #[error("no folder at {0}")] LibraryPathNotFound(String), + /// Calibre database does not exist. #[error("no metadata.db in {0}")] MetadataNotFound(String), } +/// Application configuration. pub struct Config { + /// Calibre library folder. pub library_path: PathBuf, + /// Calibre metadata file path. pub metadata_path: PathBuf, } impl Config { + /// Check if the calibre library from `args` exists and if the calibre database can be found. pub fn load(args: &Cli) -> Result { let library_path = Path::new(&args.library_path).to_path_buf(); diff --git a/rusty-library/src/data/book.rs b/rusty-library/src/data/book.rs index ac5fc12..849144e 100644 --- a/rusty-library/src/data/book.rs +++ b/rusty-library/src/data/book.rs @@ -1,17 +1,22 @@ +//! Enrich the [`Book`](struct@calibre_db::data::book::Book) type with additional information. + use std::{collections::HashMap, fmt::Display, path::Path}; use calibre_db::data::{ author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries, }; use serde::Serialize; -use time::OffsetDateTime; use crate::app_state::AppState; +/// Wrapper type for a file format string (must be a struct in order to implement traits). #[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash)] pub struct Format(pub String); +/// Wrapper type for a collection of formats, [`Format`](struct@Format) on the left and a String +/// signifying its file path on the right. pub type Formats = HashMap; +/// Recognize `pdf` and `epub` and return their value, everything else transforms to `unknown`. impl Display for Format { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.0.as_ref() { @@ -22,21 +27,30 @@ impl Display for Format { } } +/// Wrapper around [`Book`](struct@calibre_db::data::book::Book) from the +/// [`calibre-db`](mod@calibre_db) crate. Adding +/// [`Author`](struct@calibre_db::data::author::Author), +/// [`Series`](struct@calibre_db::data::series::Series) as well as +/// [`Format`](struct@Format) information. #[derive(Debug, Clone, Serialize)] pub struct Book { - pub id: u64, - pub title: String, - pub sort: String, - pub path: String, - pub uuid: String, - pub last_modified: OffsetDateTime, - pub description: Option, + /// Book data from the database. + pub data: DbBook, + /// Author information. pub author: DbAuthor, + /// Series information. Series on the left and the index of the book within + /// that series on the right. pub series: Option<(DbSeries, f64)>, + /// Format information. pub formats: Formats, } impl Book { + /// Wrap a [`DbBook`](struct@calibre_db::data::book::Book) in a [`Book`](struct@Book) and + /// enrich it with additional information. + /// + /// `db_series` is an Option tuple with the series on the left and the index of the book within + /// that series on the right. pub fn from_db_book( db_book: &DbBook, db_series: Option<(DbSeries, f64)>, @@ -44,19 +58,15 @@ impl Book { formats: Formats, ) -> Self { Self { - id: db_book.id, - title: db_book.title.clone(), - sort: db_book.sort.clone(), - path: db_book.path.clone(), - uuid: db_book.uuid.clone(), - description: db_book.description.clone(), - last_modified: db_book.last_modified, + data: db_book.clone(), author: author.clone(), series: db_series.map(|x| (x.0, x.1)), formats, } } + /// Find all pdf and epub formats of a book on disk. Search their directory in the library for + /// it. fn formats(book: &DbBook, library_path: &Path) -> Formats { let book_path = library_path.join(&book.path); let mut formats = HashMap::new(); @@ -80,6 +90,8 @@ impl Book { formats } + /// Wrap a [`DbBook`](struct@calibre_db::data::book::Book) in a [`Book`](struct@Book) by + /// fetching additional information about author, formats and series. pub fn full_book(book: &DbBook, state: &AppState) -> Option { let formats = Book::formats(book, &state.config.library_path); let author = state.calibre.book_author(book.id).ok()?; diff --git a/rusty-library/src/handlers/author.rs b/rusty-library/src/handlers/author.rs index 5617182..60f7e20 100644 --- a/rusty-library/src/handlers/author.rs +++ b/rusty-library/src/handlers/author.rs @@ -1,3 +1,5 @@ +//! Handle requests for a single author. + use std::sync::Arc; use calibre_db::data::pagination::SortOrder; @@ -9,6 +11,7 @@ use poem::{ use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; +/// Handle a request for an author with `id` and decide whether to render to html or OPDS. #[handler] pub async fn handler( id: Path, diff --git a/rusty-library/src/handlers/authors.rs b/rusty-library/src/handlers/authors.rs index 28f9e19..f6d4ba1 100644 --- a/rusty-library/src/handlers/authors.rs +++ b/rusty-library/src/handlers/authors.rs @@ -1,3 +1,5 @@ +//! Handle requests for multiple authors. + use std::sync::Arc; use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; @@ -9,6 +11,7 @@ use poem::{ use crate::{app_state::AppState, Accept}; +/// Handle a request for multiple authors, starting at the first. #[handler] pub async fn handler_init( accept: Data<&Accept>, @@ -17,6 +20,8 @@ pub async fn handler_init( authors(&accept, &state.calibre, None, &SortOrder::ASC).await } +/// Handle a request for multiple authors, starting at the `cursor` and going in the direction of +/// `sort_order`. #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, diff --git a/rusty-library/src/handlers/books.rs b/rusty-library/src/handlers/books.rs index b481a2b..9944821 100644 --- a/rusty-library/src/handlers/books.rs +++ b/rusty-library/src/handlers/books.rs @@ -1,3 +1,5 @@ +//! Handle requests for multiple books. + use std::sync::Arc; use calibre_db::data::pagination::SortOrder; @@ -9,6 +11,7 @@ use poem::{ use crate::{app_state::AppState, Accept}; +/// Handle a request for multiple books, starting at the first. #[handler] pub async fn handler_init( accept: Data<&Accept>, @@ -17,6 +20,8 @@ pub async fn handler_init( books(&accept, &state, None, &SortOrder::ASC).await } +/// Handle a request for multiple books, starting at the `cursor` and going in the direction of +/// `sort_order`. #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, diff --git a/rusty-library/src/handlers/cover.rs b/rusty-library/src/handlers/cover.rs index 022cb3d..4a0696d 100644 --- a/rusty-library/src/handlers/cover.rs +++ b/rusty-library/src/handlers/cover.rs @@ -1,3 +1,5 @@ +//! Handle requests for cover images. + use std::{fs::File, io::Read, sync::Arc}; use poem::{ @@ -9,6 +11,7 @@ use poem::{ use crate::{app_state::AppState, handlers::error::HandlerError}; +/// Handle a request for the cover image of book with id `id`. #[handler] pub async fn handler( id: Path, diff --git a/rusty-library/src/handlers/download.rs b/rusty-library/src/handlers/download.rs index 4c26d9a..6b203c4 100644 --- a/rusty-library/src/handlers/download.rs +++ b/rusty-library/src/handlers/download.rs @@ -1,3 +1,5 @@ +//! Handle requests for specific formats of a book. + use std::{fs::File, io::Read, sync::Arc}; use poem::{ @@ -13,6 +15,7 @@ use crate::{ handlers::error::HandlerError, }; +/// Handle a request for a book with id `id` in format `format`. #[handler] pub async fn handler( Path((id, format)): Path<(u64, String)>, @@ -25,7 +28,11 @@ pub async fn handler( let book = Book::full_book(&book, &state).ok_or(NotFoundError)?; let format = Format(format); let file_name = book.formats.get(&format).ok_or(NotFoundError)?; - let file_path = state.config.library_path.join(book.path).join(file_name); + let file_path = state + .config + .library_path + .join(book.data.path) + .join(file_name); let mut file = File::open(file_path).map_err(|_| NotFoundError)?; let mut data = Vec::new(); diff --git a/rusty-library/src/handlers/error.rs b/rusty-library/src/handlers/error.rs index d3f89f1..176b431 100644 --- a/rusty-library/src/handlers/error.rs +++ b/rusty-library/src/handlers/error.rs @@ -1,3 +1,5 @@ +//! Error handling for requests handlers. + use calibre_db::data::error::DataStoreError; use poem::{error::ResponseError, http::StatusCode, Body, Response}; use thiserror::Error; @@ -6,15 +8,22 @@ use uuid::Uuid; use crate::opds::error::OpdsError; +/// Errors happening during handling of requests. #[derive(Error, Debug)] #[error("opds error")] pub enum HandlerError { + /// Error rendering OPDS. #[error("opds error")] OpdsError(#[from] OpdsError), + /// Error fetching data from calibre. #[error("data error")] DataError(#[from] DataStoreError), } +/// Convert a [`HandlerError`](enum@HandlerError) into a suitable response error. +/// +/// Log the real error (internal) with an uuid and send a suitable error message to the user with +/// the same uuid (for correlation purposes). impl ResponseError for HandlerError { fn status(&self) -> StatusCode { match &self { diff --git a/rusty-library/src/handlers/html/author.rs b/rusty-library/src/handlers/html/author.rs index 477bafe..d27c6d3 100644 --- a/rusty-library/src/handlers/html/author.rs +++ b/rusty-library/src/handlers/html/author.rs @@ -1,9 +1,12 @@ +//! Handle a single author for html. + use calibre_db::data::author::Author; use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; use tera::Context; use crate::{data::book::Book, templates::TEMPLATES}; +/// Render a single author in html. pub async fn handler(author: Author, books: Vec) -> Result { let mut context = Context::new(); context.insert("title", &author.name); diff --git a/rusty-library/src/handlers/html/authors.rs b/rusty-library/src/handlers/html/authors.rs index e4d24a7..6db29d4 100644 --- a/rusty-library/src/handlers/html/authors.rs +++ b/rusty-library/src/handlers/html/authors.rs @@ -1,8 +1,11 @@ +//! Handle multiple authors in html. + use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; use poem::Response; use crate::handlers::paginated; +/// Render all authors paginated by cursor in html. pub async fn handler( calibre: &Calibre, cursor: Option<&str>, diff --git a/rusty-library/src/handlers/html/books.rs b/rusty-library/src/handlers/html/books.rs index 0100204..14700ae 100644 --- a/rusty-library/src/handlers/html/books.rs +++ b/rusty-library/src/handlers/html/books.rs @@ -1,8 +1,11 @@ +//! Handle multiple books in html. + use calibre_db::data::pagination::SortOrder; use poem::Response; use crate::{app_state::AppState, data::book::Book, handlers::paginated}; +/// Render all books paginated by cursor in html. pub async fn handler( state: &AppState, cursor: Option<&str>, @@ -16,7 +19,7 @@ pub async fn handler( .books(25, cursor, sort_order) .map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect()) }, - |book| book.sort.clone(), + |book| book.data.sort.clone(), |cursor| state.calibre.has_previous_books(cursor), |cursor| state.calibre.has_more_books(cursor), ) diff --git a/rusty-library/src/handlers/html/recent.rs b/rusty-library/src/handlers/html/recent.rs index 9f15e7b..05c142f 100644 --- a/rusty-library/src/handlers/html/recent.rs +++ b/rusty-library/src/handlers/html/recent.rs @@ -1,8 +1,11 @@ +//! Handle recent books in html. + use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; use tera::Context; use crate::{data::book::Book, templates::TEMPLATES}; +/// Render recent books as html. pub async fn handler(recent_books: Vec) -> Result { let mut context = Context::new(); context.insert("title", "Recent Books"); diff --git a/rusty-library/src/handlers/html/series.rs b/rusty-library/src/handlers/html/series.rs index d0942bb..856939d 100644 --- a/rusty-library/src/handlers/html/series.rs +++ b/rusty-library/src/handlers/html/series.rs @@ -1,8 +1,11 @@ +//! Handle multiple series in html. + use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; use poem::Response; use crate::handlers::paginated; +/// Render all series paginated by cursor as html. pub async fn handler( calibre: &Calibre, cursor: Option<&str>, diff --git a/rusty-library/src/handlers/html/series_single.rs b/rusty-library/src/handlers/html/series_single.rs index d9c1dc9..148aad4 100644 --- a/rusty-library/src/handlers/html/series_single.rs +++ b/rusty-library/src/handlers/html/series_single.rs @@ -1,9 +1,12 @@ +//! Handle a single series in html. + use calibre_db::data::series::Series; use poem::{error::InternalServerError, web::Html, IntoResponse, Response}; use tera::Context; use crate::{data::book::Book, templates::TEMPLATES}; +/// Render a single series as html. pub async fn handler(series: Series, books: Vec) -> Result { let mut context = Context::new(); context.insert("title", &series.name); diff --git a/rusty-library/src/handlers/opds/author.rs b/rusty-library/src/handlers/opds/author.rs index ccd489d..c79af76 100644 --- a/rusty-library/src/handlers/opds/author.rs +++ b/rusty-library/src/handlers/opds/author.rs @@ -1,3 +1,5 @@ +//! Handle a single author for opds. + use calibre_db::data::author::Author; use poem::{IntoResponse, Response}; use time::OffsetDateTime; @@ -8,6 +10,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render a single author as an OPDS entry embedded in a feed. pub async fn handler(author: Author, books: Vec) -> Result { let entries: Vec = books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/authors.rs b/rusty-library/src/handlers/opds/authors.rs index bc60380..afdc7aa 100644 --- a/rusty-library/src/handlers/opds/authors.rs +++ b/rusty-library/src/handlers/opds/authors.rs @@ -1,3 +1,5 @@ +//! Handle multiple authors for opds. + use calibre_db::{ calibre::Calibre, data::{author::Author as DbAuthor, pagination::SortOrder}, @@ -10,6 +12,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render all authors as OPDS entries embedded in a feed. pub async fn handler( calibre: &Calibre, _cursor: Option<&str>, diff --git a/rusty-library/src/handlers/opds/books.rs b/rusty-library/src/handlers/opds/books.rs index 0c23a8a..0e7541f 100644 --- a/rusty-library/src/handlers/opds/books.rs +++ b/rusty-library/src/handlers/opds/books.rs @@ -1,3 +1,5 @@ +//! Handle multiple books for opds. + use calibre_db::data::pagination::SortOrder; use poem::{IntoResponse, Response}; use time::OffsetDateTime; @@ -9,6 +11,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render all books as OPDS entries embedded in a feed. pub async fn handler( state: &AppState, _cursor: Option<&str>, diff --git a/rusty-library/src/handlers/opds/feed.rs b/rusty-library/src/handlers/opds/feed.rs index 8534818..1f1146e 100644 --- a/rusty-library/src/handlers/opds/feed.rs +++ b/rusty-library/src/handlers/opds/feed.rs @@ -1,3 +1,5 @@ +//! Handle the OPDS root feed. + use poem::{handler, web::WithContentType, IntoResponse}; use time::OffsetDateTime; @@ -9,6 +11,7 @@ use crate::{ }, }; +/// Render a root OPDS feed with links to the subsections (authors, books, series and recent). #[handler] pub async fn handler() -> Result, poem::Error> { let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/recent.rs b/rusty-library/src/handlers/opds/recent.rs index d3197fe..d2e0fa2 100644 --- a/rusty-library/src/handlers/opds/recent.rs +++ b/rusty-library/src/handlers/opds/recent.rs @@ -1,3 +1,5 @@ +//! Handle recent books for OPDS. + use poem::{IntoResponse, Response}; use time::OffsetDateTime; @@ -7,6 +9,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render recent books as OPDS entries embedded in a feed. pub async fn handler(recent_books: Vec) -> Result { let entries: Vec = recent_books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/series.rs b/rusty-library/src/handlers/opds/series.rs index 04598f8..afe31a7 100644 --- a/rusty-library/src/handlers/opds/series.rs +++ b/rusty-library/src/handlers/opds/series.rs @@ -1,3 +1,5 @@ +//! Handle multiple series for OPDS. + use calibre_db::{calibre::Calibre, data::pagination::SortOrder}; use poem::{IntoResponse, Response}; use time::OffsetDateTime; @@ -7,6 +9,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render all series as OPDS entries embedded in a feed. pub async fn handler( calibre: &Calibre, _cursor: Option<&str>, diff --git a/rusty-library/src/handlers/opds/series_single.rs b/rusty-library/src/handlers/opds/series_single.rs index 370ff47..ad8e5cb 100644 --- a/rusty-library/src/handlers/opds/series_single.rs +++ b/rusty-library/src/handlers/opds/series_single.rs @@ -1,3 +1,5 @@ +//! Handle a single series for opds. + use calibre_db::data::series::Series; use poem::{IntoResponse, Response}; use time::OffsetDateTime; @@ -8,6 +10,7 @@ use crate::{ opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation}, }; +/// Render a single series as an OPDS entry embedded in a feed. pub async fn handler(series: Series, books: Vec) -> Result { let entries: Vec = books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/paginated.rs b/rusty-library/src/handlers/paginated.rs index c024032..76a92af 100644 --- a/rusty-library/src/handlers/paginated.rs +++ b/rusty-library/src/handlers/paginated.rs @@ -1,3 +1,5 @@ +//! Deal with cursor pagination. + use std::fmt::Debug; use calibre_db::data::error::DataStoreError; @@ -9,6 +11,7 @@ use crate::templates::TEMPLATES; use super::error::HandlerError; +/// Render a tera template with paginated items and generate back and forth links. pub fn render( template: &str, fetcher: F, diff --git a/rusty-library/src/handlers/recent.rs b/rusty-library/src/handlers/recent.rs index ec630ef..44f268c 100644 --- a/rusty-library/src/handlers/recent.rs +++ b/rusty-library/src/handlers/recent.rs @@ -1,9 +1,12 @@ +//! Handle requests for recent books. + use std::sync::Arc; use poem::{handler, web::Data, Response}; use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; +/// Handle a request recent books and decide whether to render to html or OPDS. #[handler] pub async fn handler( accept: Data<&Accept>, diff --git a/rusty-library/src/handlers/series.rs b/rusty-library/src/handlers/series.rs index e48bcf5..9c701e9 100644 --- a/rusty-library/src/handlers/series.rs +++ b/rusty-library/src/handlers/series.rs @@ -1,3 +1,5 @@ +//! Handle requests for multiple series. + use std::sync::Arc; use calibre_db::data::pagination::SortOrder; @@ -9,6 +11,7 @@ use poem::{ use crate::{app_state::AppState, Accept}; +/// Handle a request for multiple series, starting at the first. #[handler] pub async fn handler_init( accept: Data<&Accept>, @@ -17,6 +20,8 @@ pub async fn handler_init( series(&accept, &state, None, &SortOrder::ASC).await } +/// Handle a request for multiple series, starting at the `cursor` and going in the direction of +/// `sort_order`. #[handler] pub async fn handler( Path((cursor, sort_order)): Path<(String, SortOrder)>, diff --git a/rusty-library/src/handlers/series_single.rs b/rusty-library/src/handlers/series_single.rs index 1470e79..7b41b7a 100644 --- a/rusty-library/src/handlers/series_single.rs +++ b/rusty-library/src/handlers/series_single.rs @@ -1,3 +1,5 @@ +//! Handle requests for a single series. + use std::sync::Arc; use poem::{ @@ -8,6 +10,7 @@ use poem::{ use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept}; +/// Handle a request for a series with `id` and decide whether to render to html or OPDS. #[handler] pub async fn handler( id: Path, diff --git a/rusty-library/src/lib.rs b/rusty-library/src/lib.rs index 9645fcc..f2228aa 100644 --- a/rusty-library/src/lib.rs +++ b/rusty-library/src/lib.rs @@ -1,3 +1,7 @@ +//! A very simple ebook server for a calibre library, providing a html interface as well as an OPDS feed. +//! +//! Shamelessly written to scratch my own itches. + use std::sync::Arc; use app_state::AppState; @@ -10,13 +14,17 @@ use poem::{ use rust_embed::RustEmbed; pub mod app_state; -pub mod basic_auth; pub mod cli; pub mod config; +/// Data structs and their functions. pub mod data { pub mod book; } +/// Request handlers. Because it can not be guaranteed that a proper accept header is sent, the +/// routes are doubled and the decision on whether to render html or OPDS is made with internal +/// data on the respective routes. pub mod handlers { + /// Handle requests for html. pub mod html { pub mod author; pub mod authors; @@ -25,6 +33,7 @@ pub mod handlers { pub mod series; pub mod series_single; } + /// Handle requests for OPDS. pub mod opds { pub mod author; pub mod authors; @@ -45,19 +54,34 @@ pub mod handlers { pub mod series; pub mod series_single; } -pub mod opds; +/// OPDS data structs. +pub mod opds { + pub mod author; + pub mod content; + pub mod entry; + pub mod error; + pub mod feed; + pub mod link; + pub mod media_type; + pub mod relation; +} pub mod templates; +/// Internal marker data in lieu of a proper `Accept` header. #[derive(Debug, Clone, Copy)] pub enum Accept { + /// Render as html. Html, + /// Render as OPDS. Opds, } +/// Embedd static files. #[derive(RustEmbed)] #[folder = "static"] pub struct Files; +/// Main entry point to run the ebook server with a calibre library specified in `config`. pub async fn run(config: Config) -> Result<(), std::io::Error> { let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database"); let app_state = Arc::new(AppState { calibre, config }); diff --git a/rusty-library/src/opds/author.rs b/rusty-library/src/opds/author.rs index 6350245..72ba315 100644 --- a/rusty-library/src/opds/author.rs +++ b/rusty-library/src/opds/author.rs @@ -1,10 +1,16 @@ +//! Author data. + use serde::Serialize; +/// Author information. #[derive(Debug, Serialize)] #[serde(rename = "author")] pub struct Author { + /// Full name. pub name: String, + /// Where to find the author. pub uri: String, + /// Optional email address. #[serde(skip_serializing_if = "Option::is_none")] pub email: Option, } diff --git a/rusty-library/src/opds/content.rs b/rusty-library/src/opds/content.rs index accc7ee..d9de97c 100644 --- a/rusty-library/src/opds/content.rs +++ b/rusty-library/src/opds/content.rs @@ -1,12 +1,17 @@ +//! Content data. + use serde::Serialize; use super::media_type::MediaType; +/// Content of different types, used for example for description of an entry. #[derive(Debug, Serialize)] #[serde(rename = "content")] pub struct Content { + /// Media type of this content. #[serde(rename = "@type")] pub media_type: MediaType, + /// Actual content. #[serde(rename = "$value")] pub content: String, } diff --git a/rusty-library/src/opds/entry.rs b/rusty-library/src/opds/entry.rs index 39f5345..2c601dc 100644 --- a/rusty-library/src/opds/entry.rs +++ b/rusty-library/src/opds/entry.rs @@ -1,3 +1,5 @@ +//! Entry data. + use calibre_db::data::{author::Author as DbAuthor, series::Series}; use serde::Serialize; use time::OffsetDateTime; @@ -8,21 +10,32 @@ use super::{ author::Author, content::Content, link::Link, media_type::MediaType, relation::Relation, }; +/// Fundamental piece of OPDS, holding information about entries (for example a book). #[derive(Debug, Serialize)] #[serde(rename = "entry")] pub struct Entry { + /// Title of the entry. pub title: String, + /// Id, for example a uuid. pub id: String, + /// When was this entry updated last. #[serde(with = "time::serde::rfc3339")] pub updated: OffsetDateTime, + /// Optional content. #[serde(skip_serializing_if = "Option::is_none")] + /// Optional author information. pub content: Option, #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, + /// List of links, for example to download an entry. #[serde(rename = "link")] pub links: Vec, } +/// Convert a book into an OPDS entry. +/// +/// Add the cover and formats as link with a proper media type. +/// Add author and content information. impl From for Entry { fn from(value: Book) -> Self { let author = Author { @@ -31,7 +44,7 @@ impl From for Entry { email: None, }; let mut links = vec![Link { - href: format!("/cover/{}", value.id), + href: format!("/cover/{}", value.data.id), media_type: MediaType::Jpeg, rel: Relation::Image, title: None, @@ -44,15 +57,15 @@ impl From for Entry { .collect(); links.append(&mut format_links); - let content = value.description.map(|desc| Content { + let content = value.data.description.map(|desc| Content { media_type: MediaType::Html, content: desc, }); Self { - title: value.title.clone(), - id: format!("urn:uuid:{}", value.uuid), - updated: value.last_modified, + title: value.data.title.clone(), + id: format!("urn:uuid:{}", value.data.uuid), + updated: value.data.last_modified, content, author: Some(author), links, @@ -60,6 +73,9 @@ impl From for Entry { } } +/// Convert author information into an OPDS entry. +/// +/// Add the author link. impl From for Entry { fn from(value: DbAuthor) -> Self { let links = vec![Link { @@ -81,6 +97,9 @@ impl From for Entry { } } +/// Convert series information into an OPDS entry. +/// +/// Add the series link. impl From for Entry { fn from(value: Series) -> Self { let links = vec![Link { diff --git a/rusty-library/src/opds/error.rs b/rusty-library/src/opds/error.rs index c644cab..2bc0806 100644 --- a/rusty-library/src/opds/error.rs +++ b/rusty-library/src/opds/error.rs @@ -1,15 +1,21 @@ +//! Error handling for OPDS data. + use std::string::FromUtf8Error; use quick_xml::DeError; use thiserror::Error; +/// Errors happening during handling OPDS data. #[derive(Error, Debug)] #[error("opds error")] pub enum OpdsError { + /// Error serializing OPDS data. #[error("failed to serialize struct")] SerializingError(#[from] DeError), + /// Error parsing OPDS xml structure. #[error("xml failure")] XmlError(#[from] quick_xml::Error), + /// Error decoding xml as UTF-8. #[error("failed to decode as utf-8")] Utf8Error(#[from] FromUtf8Error), } diff --git a/rusty-library/src/opds/feed.rs b/rusty-library/src/opds/feed.rs index 6d02ece..022f1cc 100644 --- a/rusty-library/src/opds/feed.rs +++ b/rusty-library/src/opds/feed.rs @@ -1,3 +1,5 @@ +//! Root feed data. + use std::io::Cursor; use quick_xml::{ @@ -13,22 +15,31 @@ use super::{ relation::Relation, }; +/// Root feed element of OPDS. #[derive(Debug, Serialize)] #[serde(rename = "feed")] pub struct Feed { + /// Title, often shown in OPDS clients. pub title: String, + /// Feed id. pub id: String, + /// When was the feed updated last. #[serde(with = "time::serde::rfc3339")] pub updated: OffsetDateTime, + /// Icon for the feed. pub icon: String, + /// Feed author. pub author: Author, + /// Links, for example home or self. #[serde(rename = "link")] pub links: Vec, + /// Entries inside the feed (books, series, subsections, ...) #[serde(rename = "entry")] pub entries: Vec, } impl Feed { + /// Create a feed with the specified data. pub fn create( now: OffsetDateTime, id: &str, @@ -65,6 +76,7 @@ impl Feed { } } + /// Serialize a feed to OPDS xml. pub fn as_xml(&self) -> Result { let xml = to_string(&self)?; let mut reader = Reader::from_str(&xml); diff --git a/rusty-library/src/opds/link.rs b/rusty-library/src/opds/link.rs index 1cfe1ba..f19e28f 100644 --- a/rusty-library/src/opds/link.rs +++ b/rusty-library/src/opds/link.rs @@ -1,32 +1,41 @@ +//! Link data. + use serde::Serialize; use crate::data::book::{Book, Format}; use super::{media_type::MediaType, relation::Relation}; +/// Link element in OPDS. #[derive(Debug, Serialize)] #[serde(rename = "link")] pub struct Link { + /// Actual hyperlink. #[serde(rename = "@href")] pub href: String, + /// Type of the target. #[serde(rename = "@type")] pub media_type: MediaType, + /// Relation of the target. #[serde(rename = "@rel")] pub rel: Relation, + /// Optional link title. #[serde(rename = "@title")] #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, + /// Optional count (how many entries at the target). #[serde(rename = "@thr:count")] #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, } +/// Convert a format from a book into a link where it is downloadable. impl From<(&Book, (&Format, &str))> for Link { fn from(value: (&Book, (&Format, &str))) -> Self { let format = value.1 .0.clone(); let media_type: MediaType = format.into(); Self { - href: format!("/book/{}/{}", value.0.id, value.1 .0), + href: format!("/book/{}/{}", value.0.data.id, value.1 .0), media_type, rel: media_type.into(), title: Some(value.1 .0 .0.clone()), diff --git a/rusty-library/src/opds/media_type.rs b/rusty-library/src/opds/media_type.rs index a9547ce..7a98869 100644 --- a/rusty-library/src/opds/media_type.rs +++ b/rusty-library/src/opds/media_type.rs @@ -1,18 +1,24 @@ +//! Media types for OPDS elements. + use serde_with::SerializeDisplay; use crate::data::book::Format; +/// Valid media types. #[derive(Debug, Copy, Clone, SerializeDisplay)] pub enum MediaType { + /// A link with this type is meant to acquire a certain thing, for example an entry. Acquisition, Epub, Html, Jpeg, + /// A link with this type is meant for navigation around a feed. Navigation, Pdf, Text, } +/// Convert `epub` and `pdf` formats to their respective media type. Everything else is `Text`. impl From for MediaType { fn from(value: Format) -> Self { match value.0.as_ref() { @@ -23,6 +29,7 @@ impl From for MediaType { } } +/// Display the respective mime types of the respective media types. impl std::fmt::Display for MediaType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/rusty-library/src/opds/mod.rs b/rusty-library/src/opds/mod.rs deleted file mode 100644 index dcd52e0..0000000 --- a/rusty-library/src/opds/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod author; -pub mod content; -pub mod entry; -pub mod error; -pub mod feed; -pub mod link; -pub mod media_type; -pub mod relation; diff --git a/rusty-library/src/opds/relation.rs b/rusty-library/src/opds/relation.rs index 52ac158..8a9a9d3 100644 --- a/rusty-library/src/opds/relation.rs +++ b/rusty-library/src/opds/relation.rs @@ -1,10 +1,14 @@ +//! Relation data. + use serde_with::SerializeDisplay; use super::media_type::MediaType; +/// Types of relations for links. #[derive(Debug, SerializeDisplay)] pub enum Relation { Image, + /// Refer to the self feed. Myself, Start, Subsection, @@ -12,6 +16,9 @@ pub enum Relation { Acquisition, } +/// Convert a media type int a relation. +/// +/// This is not always deterministic but for the ones I actually use so far it is correct. impl From for Relation { fn from(value: MediaType) -> Self { match value { @@ -26,6 +33,7 @@ impl From for Relation { } } +/// Specify how to represent all relations in OPDS. impl std::fmt::Display for Relation { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/rusty-library/src/templates.rs b/rusty-library/src/templates.rs index 532f84c..c0c7cd8 100644 --- a/rusty-library/src/templates.rs +++ b/rusty-library/src/templates.rs @@ -1,6 +1,9 @@ +//! Tera templates. + use once_cell::sync::Lazy; use tera::Tera; +/// All tera templates, globally accessible. pub static TEMPLATES: Lazy = Lazy::new(|| { let mut tera = Tera::default(); tera.add_raw_templates(vec![ diff --git a/rusty-library/templates/base.html b/rusty-library/templates/base.html index 2ade26a..d4964ef 100644 --- a/rusty-library/templates/base.html +++ b/rusty-library/templates/base.html @@ -27,7 +27,11 @@ From dc7f07476fad02e78ecc77d72e9c88bf0737ad10 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Fri, 10 May 2024 14:31:11 +0200 Subject: [PATCH 03/10] properly access book data --- rusty-library/static/style.css | 5 +++++ rusty-library/templates/base.html | 6 +++--- rusty-library/templates/book_card.html | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rusty-library/static/style.css b/rusty-library/static/style.css index e0e813b..6a9a15b 100644 --- a/rusty-library/static/style.css +++ b/rusty-library/static/style.css @@ -39,3 +39,8 @@ nav ul li { text-align: center; height: 6rem; } + +footer small { + display: flex; + justify-content: space-between; +} diff --git a/rusty-library/templates/base.html b/rusty-library/templates/base.html index d4964ef..4219f2d 100644 --- a/rusty-library/templates/base.html +++ b/rusty-library/templates/base.html @@ -28,9 +28,9 @@ diff --git a/rusty-library/templates/book_card.html b/rusty-library/templates/book_card.html index 318fb93..db491eb 100644 --- a/rusty-library/templates/book_card.html +++ b/rusty-library/templates/book_card.html @@ -1,7 +1,7 @@
-
{{ book.title }}
+
{{ book.data.title }}

{{ book.author.name }}

@@ -14,7 +14,7 @@ {% endif %}
- book cover + book cover