refactor opds to something usable
This commit is contained in:
parent
cccd3cbdc9
commit
a41dcab889
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user