quick & dirty opds implementation

This commit is contained in:
Sebastian Hugentobler 2024-05-09 12:23:26 +02:00
parent 12341d01a6
commit cccd3cbdc9
Signed by: shu
GPG key ID: BB32CF3CA052C2F0
3 changed files with 319 additions and 18 deletions

View file

@ -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<Link>,
entries: Vec<Entry>,
@ -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<AppState>>) -> Result<WithContentType<String>, poem::Error> {
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)
@ -74,14 +260,14 @@ pub async fn books(state: Data<&Arc<AppState>>) -> Result<WithContentType<String
title: None,
count: None,
};
let feed = create_feed(now, self_link, vec![], entries);
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(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
pub async fn handler() -> Result<WithContentType<String>, poem::Error> {
let now = OffsetDateTime::now_utc();
let self_link = Link {
@ -95,10 +281,10 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
title: "Books".to_string(),
id: "rusty:books".to_string(),
updated: now,
content: Content {
content: Some(Content {
media_type: MediaType::Text,
content: "Index of all books".to_string(),
},
}),
author: None,
links: vec![Link {
href: "/opds/books".to_string(),
@ -109,7 +295,68 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<Stri
}],
};
let feed = create_feed(now, self_link, vec![], vec![books_entry]);
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"))