diff --git a/rusty-library/src/handlers/opds.rs b/rusty-library/src/handlers/opds.rs
index 345ec6a..cfbc880 100644
--- a/rusty-library/src/handlers/opds.rs
+++ b/rusty-library/src/handlers/opds.rs
@@ -1,9 +1,9 @@
use std::sync::Arc;
-use calibre_db::data::pagination::SortOrder;
+use calibre_db::data::{author::Author as DbAuthor, pagination::SortOrder};
use poem::{
handler,
- web::{Data, WithContentType},
+ web::{Data, Path, WithContentType},
IntoResponse,
};
use time::OffsetDateTime;
@@ -20,6 +20,8 @@ use crate::{
fn create_feed(
now: OffsetDateTime,
+ id: &str,
+ title: &str,
self_link: Link,
mut additional_links: Vec,
entries: Vec,
@@ -42,8 +44,8 @@ fn create_feed(
links.append(&mut additional_links);
Feed {
- title: "rusty-library".to_string(),
- id: "rusty:catalog".to_string(),
+ title: title.to_string(),
+ id: id.to_string(),
updated: now,
icon: "favicon.ico".to_string(),
author,
@@ -53,7 +55,191 @@ fn create_feed(
}
#[handler]
-pub async fn books(state: Data<&Arc>) -> Result, poem::Error> {
+pub async fn recents_handler(
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
+ let books = state
+ .calibre
+ .recent_books(25)
+ .map_err(HandlerError::DataError)?;
+ let books = books
+ .iter()
+ .filter_map(|x| Book::full_book(x, &state))
+ .collect::>();
+
+ let entries: Vec = books.into_iter().map(Entry::from).collect();
+ let now = OffsetDateTime::now_utc();
+
+ let self_link = Link {
+ href: "/opds/recent".to_string(),
+ media_type: MediaType::Navigation,
+ rel: Relation::Myself,
+ title: None,
+ count: None,
+ };
+ let feed = create_feed(
+ now,
+ "rusty:recentbooks",
+ "Recent Books",
+ self_link,
+ vec![],
+ entries,
+ );
+ let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
+
+ Ok(xml.with_content_type("application/atom+xml"))
+}
+
+#[handler]
+pub async fn series_single_handler(
+ id: Path,
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
+ let series = state
+ .calibre
+ .scalar_series(*id)
+ .map_err(HandlerError::DataError)?;
+ let books = state
+ .calibre
+ .series_books(*id)
+ .map_err(HandlerError::DataError)?;
+ let books = books
+ .iter()
+ .filter_map(|x| Book::full_book(x, &state))
+ .collect::>();
+
+ let entries: Vec = books.into_iter().map(Entry::from).collect();
+ let now = OffsetDateTime::now_utc();
+
+ let self_link = Link {
+ href: format!("/opds/series/{}", *id),
+ media_type: MediaType::Navigation,
+ rel: Relation::Myself,
+ title: None,
+ count: None,
+ };
+ let feed = create_feed(
+ now,
+ &format!("rusty:series:{}", *id),
+ &series.name,
+ self_link,
+ vec![],
+ entries,
+ );
+ let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
+
+ Ok(xml.with_content_type("application/atom+xml"))
+}
+
+#[handler]
+pub async fn series_handler(
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
+ let series = state
+ .calibre
+ .series(u32::MAX.into(), None, &SortOrder::ASC)
+ .map_err(HandlerError::DataError)?;
+
+ let entries: Vec = series.into_iter().map(Entry::from).collect();
+ let now = OffsetDateTime::now_utc();
+
+ let self_link = Link {
+ href: "/opds/series".to_string(),
+ media_type: MediaType::Navigation,
+ rel: Relation::Myself,
+ title: None,
+ count: None,
+ };
+ let feed = create_feed(
+ now,
+ "rusty:series",
+ "All Series",
+ self_link,
+ vec![],
+ entries,
+ );
+ let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
+
+ Ok(xml.with_content_type("application/atom+xml"))
+}
+
+#[handler]
+pub async fn author_handler(
+ id: Path,
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
+ let author = state
+ .calibre
+ .scalar_author(*id)
+ .map_err(HandlerError::DataError)?;
+ let books = state
+ .calibre
+ .author_books(*id, u32::MAX.into(), None, SortOrder::ASC)
+ .map_err(HandlerError::DataError)?;
+ let books = books
+ .iter()
+ .filter_map(|x| Book::full_book(x, &state))
+ .collect::>();
+
+ let entries: Vec = books.into_iter().map(Entry::from).collect();
+ let now = OffsetDateTime::now_utc();
+
+ let self_link = Link {
+ href: format!("/opds/authors/{}", author.id),
+ media_type: MediaType::Navigation,
+ rel: Relation::Myself,
+ title: None,
+ count: None,
+ };
+ let feed = create_feed(
+ now,
+ &format!("rusty:author:{}", author.id),
+ &author.name,
+ self_link,
+ vec![],
+ entries,
+ );
+ let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
+
+ Ok(xml.with_content_type("application/atom+xml"))
+}
+
+#[handler]
+pub async fn authors_handler(
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
+ let authors: Vec = state
+ .calibre
+ .authors(u32::MAX.into(), None, &SortOrder::ASC)
+ .map_err(HandlerError::DataError)?;
+
+ let entries: Vec = authors.into_iter().map(Entry::from).collect();
+ let now = OffsetDateTime::now_utc();
+
+ let self_link = Link {
+ href: "/opds/authors".to_string(),
+ media_type: MediaType::Navigation,
+ rel: Relation::Myself,
+ title: None,
+ count: None,
+ };
+ let feed = create_feed(
+ now,
+ "rusty:authors",
+ "All Authors",
+ self_link,
+ vec![],
+ entries,
+ );
+ let xml = feed.as_xml().map_err(HandlerError::OpdsError)?;
+
+ Ok(xml.with_content_type("application/atom+xml"))
+}
+
+#[handler]
+pub async fn books_handler(
+ state: Data<&Arc>,
+) -> Result, poem::Error> {
let books: Vec = state
.calibre
.books(u32::MAX.into(), None, &SortOrder::ASC)
@@ -74,14 +260,14 @@ pub async fn books(state: Data<&Arc>) -> Result>) -> Result, poem::Error> {
+pub async fn handler() -> Result, poem::Error> {
let now = OffsetDateTime::now_utc();
let self_link = Link {
@@ -95,10 +281,10 @@ pub async fn handler(state: Data<&Arc>) -> Result>) -> Result Result<(), std::io::Error> {
let app = Route::new()
.at("/", get(handlers::recents::handler))
.at("/opds", get(handlers::opds::handler))
- .at("/opds/books", get(handlers::opds::books))
+ .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),
+ )
.at("/books", get(handlers::books::handler_init))
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
.at("/series", get(handlers::series::handler_init))
diff --git a/rusty-library/src/opds/entry.rs b/rusty-library/src/opds/entry.rs
index dd63192..ef518ed 100644
--- a/rusty-library/src/opds/entry.rs
+++ b/rusty-library/src/opds/entry.rs
@@ -1,3 +1,4 @@
+use calibre_db::data::{author::Author as DbAuthor, series::Series};
use serde::Serialize;
use time::OffsetDateTime;
@@ -14,7 +15,8 @@ pub struct Entry {
pub id: String,
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
- pub content: Content,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub content: Option,
pub author: Option,
#[serde(rename = "link")]
pub links: Vec,
@@ -41,20 +43,64 @@ impl From for Entry {
.collect();
links.append(&mut format_links);
+ let content = value.description.map(|desc| Content {
+ media_type: MediaType::Html,
+ content: desc,
+ });
+
Self {
title: value.title.clone(),
id: format!("urn:uuid:{}", value.uuid),
updated: value.last_modified,
- content: Content {
- media_type: MediaType::Html,
- content: value.description.clone().unwrap_or("".to_string()),
- },
+ content,
author: Some(author),
links,
}
}
}
+impl From for Entry {
+ fn from(value: DbAuthor) -> Self {
+ let links = vec![Link {
+ href: format!("/opds/authors/{}", value.id),
+ media_type: MediaType::Acquisition,
+ rel: Relation::Subsection,
+ title: None,
+ count: None,
+ }];
+
+ Self {
+ title: value.name.clone(),
+ id: format!("rusty:authors:{}", value.id),
+ updated: OffsetDateTime::now_utc(),
+ content: None,
+ author: None,
+ links,
+ }
+ }
+}
+
+impl From for Entry {
+ fn from(value: Series) -> Self {
+ let links = vec![Link {
+ href: format!("/opds/series/{}", value.id),
+ media_type: MediaType::Acquisition,
+ rel: Relation::Subsection,
+ title: None,
+ count: None,
+ }];
+
+ Self {
+ title: value.name.clone(),
+ id: format!("rusty:series:{}", value.id),
+ updated: OffsetDateTime::now_utc(),
+ content: None,
+ author: None,
+ links,
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use quick_xml::se::to_string;
@@ -69,10 +115,10 @@ mod tests {
title: "Authors".to_string(),
id: "rust:authors".to_string(),
updated: datetime!(2024-05-06 19:14:54 UTC),
- content: Content {
+ content: Some(Content {
media_type: MediaType::Text,
content: "All authors".to_string(),
- },
+ }),
author: None,
links: vec![
Link {