refactor opds to something usable
This commit is contained in:
parent
cccd3cbdc9
commit
a41dcab889
25 changed files with 636 additions and 501 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
18
rusty-library/src/handlers/html/author.rs
Normal file
18
rusty-library/src/handlers/html/author.rs
Normal 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())
|
||||
}
|
18
rusty-library/src/handlers/html/authors.rs
Normal file
18
rusty-library/src/handlers/html/authors.rs
Normal 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),
|
||||
)
|
||||
}
|
23
rusty-library/src/handlers/html/books.rs
Normal file
23
rusty-library/src/handlers/html/books.rs
Normal 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),
|
||||
)
|
||||
}
|
17
rusty-library/src/handlers/html/recent.rs
Normal file
17
rusty-library/src/handlers/html/recent.rs
Normal 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())
|
||||
}
|
18
rusty-library/src/handlers/html/series.rs
Normal file
18
rusty-library/src/handlers/html/series.rs
Normal 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),
|
||||
)
|
||||
}
|
18
rusty-library/src/handlers/html/series_single.rs
Normal file
18
rusty-library/src/handlers/html/series_single.rs
Normal 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())
|
||||
}
|
|
@ -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"))
|
||||
}
|
35
rusty-library/src/handlers/opds/author.rs
Normal file
35
rusty-library/src/handlers/opds/author.rs
Normal 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())
|
||||
}
|
45
rusty-library/src/handlers/opds/authors.rs
Normal file
45
rusty-library/src/handlers/opds/authors.rs
Normal 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())
|
||||
}
|
39
rusty-library/src/handlers/opds/books.rs
Normal file
39
rusty-library/src/handlers/opds/books.rs
Normal 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())
|
||||
}
|
106
rusty-library/src/handlers/opds/feed.rs
Normal file
106
rusty-library/src/handlers/opds/feed.rs
Normal 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"))
|
||||
}
|
34
rusty-library/src/handlers/opds/recent.rs
Normal file
34
rusty-library/src/handlers/opds/recent.rs
Normal 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())
|
||||
}
|
42
rusty-library/src/handlers/opds/series.rs
Normal file
42
rusty-library/src/handlers/opds/series.rs
Normal 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())
|
||||
}
|
35
rusty-library/src/handlers/opds/series_single.rs
Normal file
35
rusty-library/src/handlers/opds/series_single.rs
Normal 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())
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
25
rusty-library/src/handlers/recent.rs
Normal file
25
rusty-library/src/handlers/recent.rs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue