From a41dcab889e656b00c895d736c402dd54fcef4ec Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Thu, 9 May 2024 14:24:45 +0200 Subject: [PATCH] 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);