This commit is contained in:
Sebastian Hugentobler 2024-05-08 18:11:39 +02:00
parent 47e8c74419
commit faea154ff5
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
12 changed files with 533 additions and 6 deletions

180
Cargo.lock generated
View File

@ -135,6 +135,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -216,6 +222,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
"windows-targets 0.52.5",
]
@ -260,7 +267,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"strsim 0.11.1",
]
[[package]]
@ -337,6 +344,51 @@ dependencies = [
"typenum",
]
[[package]]
name = "darling"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "deunicode"
version = "1.4.4"
@ -502,13 +554,19 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 2.2.6",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -525,7 +583,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
dependencies = [
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@ -534,7 +592,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64",
"base64 0.21.7",
"bytes",
"headers-core",
"http",
@ -684,6 +742,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "ignore"
version = "0.4.22"
@ -700,6 +764,17 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@ -707,7 +782,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.5",
"serde",
]
[[package]]
@ -840,6 +916,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1068,6 +1150,12 @@ dependencies = [
"syn",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -1092,6 +1180,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "quote"
version = "1.0.36"
@ -1262,11 +1360,14 @@ dependencies = [
"clap",
"once_cell",
"poem",
"quick-xml",
"rust-embed",
"serde",
"serde_json",
"serde_with",
"tera",
"thiserror",
"time",
"tokio",
"tracing",
"tracing-subscriber",
@ -1346,6 +1447,36 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.2.6",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha1"
version = "0.10.6"
@ -1418,6 +1549,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
@ -1496,6 +1633,37 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tokio"
version = "1.37.0"
@ -1549,7 +1717,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"toml_datetime",
"winnow",
]

View File

@ -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"] }

View 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"))
}

View File

@ -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))

View 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>");
}
}

View 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,
}

View 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>"#
);
}
}

View 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()
}
}

View 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"/>"#
);
}
}

View 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"),
}
}
}

View 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;

View 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"),
}
}
}