opds wip
This commit is contained in:
parent
47e8c74419
commit
faea154ff5
12 changed files with 533 additions and 6 deletions
|
@ -12,9 +12,12 @@ poem = { version = "3.0.0", features = ["embed", "static-files"] }
|
|||
rust-embed = "8.3.0"
|
||||
serde = { workspace = true }
|
||||
serde_json = "1.0.116"
|
||||
serde_with = "3.8.1"
|
||||
tera = "1.19.1"
|
||||
thiserror = { workspace = true }
|
||||
time = { version = "0.3.36", features = ["macros", "serde", "formatting"] }
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng"] }
|
||||
quick-xml = { version = "0.31.0", features = ["serialize"] }
|
||||
|
|
68
rusty-library/src/handlers/opds.rs
Normal file
68
rusty-library/src/handlers/opds.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::{
|
||||
handler,
|
||||
web::{headers::ContentType, Data, WithContentType},
|
||||
IntoResponse,
|
||||
};
|
||||
use quick_xml::se::to_string;
|
||||
use time::macros::datetime;
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
opds::{
|
||||
author::Author, content::Content, entry::Entry, feed::Feed, link::Link,
|
||||
media_type::MediaType, relation::Relation,
|
||||
},
|
||||
};
|
||||
|
||||
#[handler]
|
||||
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<WithContentType<String>, poem::Error> {
|
||||
let author = Author {
|
||||
name: "Thallian".to_string(),
|
||||
uri: "https://code.vanwa.ch/shu/rusty-library".to_string(),
|
||||
email: None,
|
||||
};
|
||||
let home_link = Link {
|
||||
href: "/opds".to_string(),
|
||||
media_type: MediaType::Navigation,
|
||||
rel: Relation::Start,
|
||||
title: Some("Home".to_string()),
|
||||
count: None,
|
||||
};
|
||||
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: datetime!(2024-05-06 19:14:54 UTC),
|
||||
content: Content {
|
||||
media_type: MediaType::Text,
|
||||
content: "Index of all books".to_string(),
|
||||
},
|
||||
links: vec![Link {
|
||||
href: "/opds/books".to_string(),
|
||||
media_type: MediaType::Navigation,
|
||||
rel: Relation::Subsection,
|
||||
title: None,
|
||||
count: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let feed = Feed {
|
||||
title: "rusty-library".to_string(),
|
||||
id: "rusty:catalog".to_string(),
|
||||
updated: datetime!(2024-05-06 19:14:54 UTC),
|
||||
icon: "favicon.ico".to_string(),
|
||||
author,
|
||||
links: vec![home_link, self_link],
|
||||
entries: vec![books_entry],
|
||||
};
|
||||
let xml = feed.as_xml();
|
||||
Ok(xml.with_content_type("application/atom+xml"))
|
||||
}
|
|
@ -24,11 +24,13 @@ mod handlers {
|
|||
pub mod cover;
|
||||
pub mod download;
|
||||
pub mod error;
|
||||
pub mod opds;
|
||||
pub mod paginated;
|
||||
pub mod recents;
|
||||
pub mod series;
|
||||
pub mod series_single;
|
||||
}
|
||||
mod opds;
|
||||
mod templates;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
|
@ -50,6 +52,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||
|
||||
let app = Route::new()
|
||||
.at("/", get(handlers::recents::handler))
|
||||
.at("/opds", get(handlers::opds::handler))
|
||||
.at("/books", get(handlers::books::handler_init))
|
||||
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
||||
.at("/series", get(handlers::series::handler_init))
|
||||
|
|
32
rusty-library/src/opds/author.rs
Normal file
32
rusty-library/src/opds/author.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "author")]
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
pub uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quick_xml::se::to_string;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn init() -> Author {
|
||||
Author {
|
||||
name: "Rohal der Weise".to_string(),
|
||||
uri: "https://de.wiki-aventurica.de/wiki/Rohal_der_Weise".to_string(),
|
||||
email: Some("rohal@aventurien.de".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let author = init();
|
||||
let xml = to_string(&author).unwrap();
|
||||
assert_eq!(xml, "<author><name>Rohal der Weise</name><uri>https://de.wiki-aventurica.de/wiki/Rohal_der_Weise</uri><email>rohal@aventurien.de</email></author>");
|
||||
}
|
||||
}
|
12
rusty-library/src/opds/content.rs
Normal file
12
rusty-library/src/opds/content.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::media_type::MediaType;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "content")]
|
||||
pub struct Content {
|
||||
#[serde(rename = "@type")]
|
||||
pub media_type: MediaType,
|
||||
#[serde(rename = "$value")]
|
||||
pub content: String,
|
||||
}
|
64
rusty-library/src/opds/entry.rs
Normal file
64
rusty-library/src/opds/entry.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{content::Content, link::Link};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "entry")]
|
||||
pub struct Entry {
|
||||
pub title: String,
|
||||
pub id: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated: OffsetDateTime,
|
||||
pub content: Content,
|
||||
#[serde(rename = "link")]
|
||||
pub links: Vec<Link>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quick_xml::se::to_string;
|
||||
use time::macros::datetime;
|
||||
|
||||
use crate::opds::{content::Content, media_type::MediaType, relation::Relation};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn init() -> Entry {
|
||||
Entry {
|
||||
title: "Authors".to_string(),
|
||||
id: "rust:authors".to_string(),
|
||||
updated: datetime!(2024-05-06 19:14:54 UTC),
|
||||
content: Content {
|
||||
media_type: MediaType::Text,
|
||||
content: "All authors".to_string(),
|
||||
},
|
||||
links: vec![
|
||||
Link {
|
||||
href: "/opds".to_string(),
|
||||
media_type: MediaType::Text,
|
||||
rel: Relation::Start,
|
||||
title: None,
|
||||
count: None,
|
||||
},
|
||||
Link {
|
||||
href: "/opds".to_string(),
|
||||
media_type: MediaType::Text,
|
||||
rel: Relation::Start,
|
||||
title: None,
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let entry = init();
|
||||
let xml = to_string(&entry).unwrap();
|
||||
assert_eq!(
|
||||
xml,
|
||||
r#"<entry><title>Authors</title><id>rust:authors</id><updated>2024-05-06T19:14:54Z</updated><content type="text">All authors</content><link href="/opds" type="text" rel="start"/><link href="/opds" type="text" rel="start"/></entry>"#
|
||||
);
|
||||
}
|
||||
}
|
59
rusty-library/src/opds/feed.rs
Normal file
59
rusty-library/src/opds/feed.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use std::io::Cursor;
|
||||
|
||||
use quick_xml::{
|
||||
events::{BytesDecl, BytesStart, Event},
|
||||
se::to_string,
|
||||
Reader, Writer,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::{author::Author, entry::Entry, link::Link};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "feed")]
|
||||
pub struct Feed {
|
||||
pub title: String,
|
||||
pub id: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated: OffsetDateTime,
|
||||
pub icon: String,
|
||||
pub author: Author,
|
||||
#[serde(rename = "link")]
|
||||
pub links: Vec<Link>,
|
||||
#[serde(rename = "entry")]
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
impl Feed {
|
||||
pub fn as_xml(&self) -> String {
|
||||
let xml = to_string(&self).unwrap();
|
||||
let mut reader = Reader::from_str(&xml);
|
||||
reader.trim_text(true);
|
||||
|
||||
let declaration = BytesDecl::new("1.0", Some("UTF-8"), None);
|
||||
let mut writer = Writer::new(Cursor::new(Vec::new()));
|
||||
writer.write_event(Event::Decl(declaration)).unwrap();
|
||||
|
||||
let mut feed_start = BytesStart::new("feed");
|
||||
feed_start.push_attribute(("xmlns", "http://www.w3.org/2005/Atom"));
|
||||
feed_start.push_attribute(("xmlns:xhtml", "http://www.w3.org/1999/xhtml"));
|
||||
feed_start.push_attribute(("xmlns:opds", "http://opds-spec.org/2010/catalog"));
|
||||
feed_start.push_attribute(("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/"));
|
||||
feed_start.push_attribute(("xmlns:dcterms", "http://purl.org/dc/terms/"));
|
||||
feed_start.push_attribute(("xmlns:thr", "http://purl.org/syndication/thread/1.0"));
|
||||
|
||||
loop {
|
||||
match reader.read_event() {
|
||||
Ok(Event::Start(e)) if e.name().as_ref() == b"feed" => writer
|
||||
.write_event(Event::Start(feed_start.clone()))
|
||||
.unwrap(),
|
||||
Ok(Event::Eof) => break,
|
||||
Ok(e) => writer.write_event(e).unwrap(),
|
||||
Err(e) => (),
|
||||
}
|
||||
}
|
||||
let result = writer.into_inner().into_inner();
|
||||
String::from_utf8(result).unwrap()
|
||||
}
|
||||
}
|
57
rusty-library/src/opds/link.rs
Normal file
57
rusty-library/src/opds/link.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::{media_type::MediaType, relation::Relation};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename = "link")]
|
||||
pub struct Link {
|
||||
#[serde(rename = "@href")]
|
||||
pub href: String,
|
||||
#[serde(rename = "@type")]
|
||||
pub media_type: MediaType,
|
||||
#[serde(rename = "@rel")]
|
||||
pub rel: Relation,
|
||||
#[serde(rename = "@title")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(rename = "@thr:count")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub count: Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quick_xml::se::to_string;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn init() -> Link {
|
||||
Link {
|
||||
href: "/opds".to_string(),
|
||||
media_type: MediaType::Navigation,
|
||||
rel: Relation::Start,
|
||||
title: Some("Home".to_string()),
|
||||
count: Some(10),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let mut link = init();
|
||||
let xml = to_string(&link).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
xml,
|
||||
r#"<link href="/opds" type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="start" title="Home" thr:count="10"/>"#
|
||||
);
|
||||
|
||||
link.title = None;
|
||||
link.count = None;
|
||||
let xml = to_string(&link).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
xml,
|
||||
r#"<link href="/opds" type="application/atom+xml;profile=opds-catalog;kind=navigation" rel="start"/>"#
|
||||
);
|
||||
}
|
||||
}
|
32
rusty-library/src/opds/media_type.rs
Normal file
32
rusty-library/src/opds/media_type.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(Debug, SerializeDisplay)]
|
||||
pub enum MediaType {
|
||||
Acquisition,
|
||||
Epub,
|
||||
Html,
|
||||
Jpeg,
|
||||
Navigation,
|
||||
Pdf,
|
||||
Text,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MediaType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MediaType::Acquisition => write!(
|
||||
f,
|
||||
"application/atom+xml;profile=opds-catalog;kind=aqcuisition"
|
||||
),
|
||||
MediaType::Epub => write!(f, "application/epub+zip"),
|
||||
MediaType::Html => write!(f, "text/html"),
|
||||
MediaType::Jpeg => write!(f, "image/jpeg"),
|
||||
MediaType::Navigation => write!(
|
||||
f,
|
||||
"application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
),
|
||||
MediaType::Pdf => write!(f, "application/pdf"),
|
||||
MediaType::Text => write!(f, "text"),
|
||||
}
|
||||
}
|
||||
}
|
7
rusty-library/src/opds/mod.rs
Normal file
7
rusty-library/src/opds/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod author;
|
||||
pub mod content;
|
||||
pub mod entry;
|
||||
pub mod feed;
|
||||
pub mod link;
|
||||
pub mod media_type;
|
||||
pub mod relation;
|
22
rusty-library/src/opds/relation.rs
Normal file
22
rusty-library/src/opds/relation.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use serde_with::SerializeDisplay;
|
||||
|
||||
#[derive(Debug, SerializeDisplay)]
|
||||
pub enum Relation {
|
||||
Image,
|
||||
Myself,
|
||||
Start,
|
||||
Subsection,
|
||||
Thumbnail,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Relation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Relation::Image => write!(f, "http://opds-spec.org/image"),
|
||||
Relation::Myself => write!(f, "self"),
|
||||
Relation::Start => write!(f, "start"),
|
||||
Relation::Subsection => write!(f, "subsection"),
|
||||
Relation::Thumbnail => write!(f, "http://opds-spec.org/image/thumbnail"),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue