refactor opds to something usable

This commit is contained in:
Sebastian Hugentobler 2024-05-09 14:24:45 +02:00
parent cccd3cbdc9
commit a41dcab889
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
25 changed files with 636 additions and 501 deletions

View File

@ -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<u64>,
accept: Data<&Accept>,
state: Data<&Arc<AppState>>,
) -> Result<Html<String>, poem::Error> {
) -> Result<Response, poem::Error> {
let author = state
.calibre
.scalar_author(*id)
@ -30,13 +28,8 @@ pub async fn handler(
.filter_map(|x| Book::full_book(x, &state))
.collect::<Vec<Book>>();
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,
}
}

View File

@ -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<AppState>>) -> Result<Html<String>, poem::Error> {
authors(&state.calibre, None, &SortOrder::ASC)
pub async fn handler_init(
accept: Data<&Accept>,
state: Data<&Arc<AppState>>,
) -> Result<Response, poem::Error> {
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<AppState>>,
) -> Result<Html<String>, poem::Error> {
authors(&state.calibre, Some(&cursor), &sort_order)
) -> Result<Response, poem::Error> {
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<Html<String>, 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<Response, poem::Error> {
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,
}
}

View File

@ -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<AppState>>) -> Result<Html<String>, poem::Error> {
books(&state, None, &SortOrder::ASC)
pub async fn handler_init(
accept: Data<&Accept>,
state: Data<&Arc<AppState>>,
) -> Result<Response, poem::Error> {
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<AppState>>,
) -> Result<Html<String>, poem::Error> {
books(&state, Some(&cursor), &sort_order)
) -> Result<Response, poem::Error> {
books(&accept, &state, Some(&cursor), &sort_order).await
}
fn books(
async fn books(
accept: &Accept,
state: &Arc<AppState>,
cursor: Option<&str>,
sort_order: &SortOrder,
) -> Result<Html<String>, 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<Response, poem::Error> {
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,
}
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
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())
}

View File

@ -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<Response, 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),
)
}

View File

@ -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<Response, 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),
)
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
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())
}

View File

@ -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<Response, poem::Error> {
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),
)
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
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())
}

View File

@ -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<Link>,
entries: Vec<Entry>,
) -> 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<AppState>>,
) -> Result<WithContentType<String>, 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::<Vec<Book>>();
let entries: Vec<Entry> = 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<u64>,
state: Data<&Arc<AppState>>,
) -> Result<WithContentType<String>, 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::<Vec<Book>>();
let entries: Vec<Entry> = 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<AppState>>,
) -> Result<WithContentType<String>, poem::Error> {
let series = state
.calibre
.series(u32::MAX.into(), None, &SortOrder::ASC)
.map_err(HandlerError::DataError)?;
let entries: Vec<Entry> = 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<u64>,
state: Data<&Arc<AppState>>,
) -> Result<WithContentType<String>, 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::<Vec<Book>>();
let entries: Vec<Entry> = 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<AppState>>,
) -> Result<WithContentType<String>, poem::Error> {
let authors: Vec<DbAuthor> = state
.calibre
.authors(u32::MAX.into(), None, &SortOrder::ASC)
.map_err(HandlerError::DataError)?;
let entries: Vec<Entry> = 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<AppState>>,
) -> Result<WithContentType<String>, poem::Error> {
let books: Vec<Book> = 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<Entry> = 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<WithContentType<String>, 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"))
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = 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())
}

View File

@ -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<Response, poem::Error> {
let authors: Vec<DbAuthor> = calibre
.authors(u32::MAX.into(), None, &SortOrder::ASC)
.map_err(HandlerError::DataError)?;
let entries: Vec<Entry> = 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())
}

View File

@ -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<Response, poem::Error> {
let books: Vec<Book> = 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<Entry> = 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())
}

View File

@ -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<WithContentType<String>, 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"))
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = 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())
}

View File

@ -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<Response, poem::Error> {
let series = calibre
.series(u32::MAX.into(), None, &SortOrder::ASC)
.map_err(HandlerError::DataError)?;
let entries: Vec<Entry> = 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())
}

View File

@ -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<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = 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())
}

View File

@ -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<T: Serialize + Debug, F, S, P, M>(
sort_field: S,
has_previous: P,
has_more: M,
) -> Result<Html<String>, poem::Error>
) -> Result<Response, poem::Error>
where
F: Fn() -> Result<Vec<T>, 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())
}

View File

@ -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<AppState>>,
) -> Result<Response, 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::<Vec<Book>>();
match accept.0 {
Accept::Html => crate::handlers::html::recent::handler(recent_books).await,
Accept::Opds => crate::handlers::opds::recent::handler(recent_books).await,
}
}

View File

@ -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<AppState>>) -> Result<Html<String>, 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::<Vec<Book>>();
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)
}

View File

@ -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<AppState>>) -> Result<Html<String>, poem::Error> {
series(&state, None, &SortOrder::ASC)
pub async fn handler_init(
accept: Data<&Accept>,
state: Data<&Arc<AppState>>,
) -> Result<Response, poem::Error> {
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<AppState>>,
) -> Result<Html<String>, poem::Error> {
series(&state, Some(&cursor), &sort_order)
) -> Result<Response, poem::Error> {
series(&accept, &state, Some(&cursor), &sort_order).await
}
fn series(
async fn series(
accept: &Accept,
state: &Arc<AppState>,
cursor: Option<&str>,
sort_order: &SortOrder,
) -> Result<Html<String>, 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<Response, poem::Error> {
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
}
}
}

View File

@ -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<u64>,
accept: Data<&Accept>,
state: Data<&Arc<AppState>>,
) -> Result<Html<String>, poem::Error> {
) -> Result<Response, poem::Error> {
let series = state
.calibre
.scalar_series(*id)
@ -29,13 +27,8 @@ pub async fn handler(
.filter_map(|x| Book::full_book(x, &state))
.collect::<Vec<Book>>();
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,
}
}

View File

@ -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::<Files>::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);

View File

@ -17,6 +17,7 @@ pub struct Entry {
pub updated: OffsetDateTime,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<Author>,
#[serde(rename = "link")]
pub links: Vec<Link>,

View File

@ -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<Link>,
entries: Vec<Entry>,
) -> 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<String, OpdsError> {
let xml = to_string(&self)?;
let mut reader = Reader::from_str(&xml);