opds wip
This commit is contained in:
parent
47e8c74419
commit
faea154ff5
180
Cargo.lock
generated
180
Cargo.lock
generated
@ -135,6 +135,12 @@ version = "0.21.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -216,6 +222,7 @@ dependencies = [
|
|||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"windows-targets 0.52.5",
|
"windows-targets 0.52.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -260,7 +267,7 @@ dependencies = [
|
|||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim",
|
"strsim 0.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -337,6 +344,51 @@ dependencies = [
|
|||||||
"typenum",
|
"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]]
|
[[package]]
|
||||||
name = "deunicode"
|
name = "deunicode"
|
||||||
version = "1.4.4"
|
version = "1.4.4"
|
||||||
@ -502,13 +554,19 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"indexmap",
|
"indexmap 2.2.6",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
@ -525,7 +583,7 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
|
checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -534,7 +592,7 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core",
|
||||||
"http",
|
"http",
|
||||||
@ -684,6 +742,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ignore"
|
name = "ignore"
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
@ -700,6 +764,17 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.2.6"
|
version = "2.2.6"
|
||||||
@ -707,7 +782,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.14.5",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -840,6 +916,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@ -1068,6 +1150,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@ -1092,6 +1180,16 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.36"
|
version = "1.0.36"
|
||||||
@ -1262,11 +1360,14 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"poem",
|
"poem",
|
||||||
|
"quick-xml",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"tera",
|
"tera",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@ -1346,6 +1447,36 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@ -1418,6 +1549,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -1496,6 +1633,37 @@ dependencies = [
|
|||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.37.0"
|
version = "1.37.0"
|
||||||
@ -1549,7 +1717,7 @@ version = "0.21.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.2.6",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
@ -12,9 +12,12 @@ poem = { version = "3.0.0", features = ["embed", "static-files"] }
|
|||||||
rust-embed = "8.3.0"
|
rust-embed = "8.3.0"
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
|
serde_with = "3.8.1"
|
||||||
tera = "1.19.1"
|
tera = "1.19.1"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
time = { version = "0.3.36", features = ["macros", "serde", "formatting"] }
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
uuid = { version = "1.8.0", features = ["v4", "fast-rng"] }
|
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 cover;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod opds;
|
||||||
pub mod paginated;
|
pub mod paginated;
|
||||||
pub mod recents;
|
pub mod recents;
|
||||||
pub mod series;
|
pub mod series;
|
||||||
pub mod series_single;
|
pub mod series_single;
|
||||||
}
|
}
|
||||||
|
mod opds;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
@ -50,6 +52,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
|
|
||||||
let app = Route::new()
|
let app = Route::new()
|
||||||
.at("/", get(handlers::recents::handler))
|
.at("/", get(handlers::recents::handler))
|
||||||
|
.at("/opds", get(handlers::opds::handler))
|
||||||
.at("/books", get(handlers::books::handler_init))
|
.at("/books", get(handlers::books::handler_init))
|
||||||
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
||||||
.at("/series", get(handlers::series::handler_init))
|
.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…
Reference in New Issue
Block a user