From cccd3cbdc9fd3c99c2a9036e82f4ae5ec6658e1c Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Thu, 9 May 2024 12:23:26 +0200 Subject: [PATCH] quick & dirty opds implementation --- rusty-library/src/handlers/opds.rs | 267 +++++++++++++++++++++++++++-- rusty-library/src/lib.rs | 10 +- rusty-library/src/opds/entry.rs | 60 ++++++- 3 files changed, 319 insertions(+), 18 deletions(-) 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 {