add lost opds root feed
This commit is contained in:
parent
9bf3a22bf5
commit
491598aaaf
6 changed files with 147 additions and 5 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -305,7 +305,7 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "calibre-db"
|
name = "calibre-db"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"r2d2_sqlite",
|
"r2d2_sqlite",
|
||||||
|
@ -1222,7 +1222,7 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "little-hesinde"
|
name = "little-hesinde"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"calibre-db",
|
"calibre-db",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "calibre-db"
|
name = "calibre-db"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "little-hesinde"
|
name = "little-hesinde"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
authors = { workspace = true }
|
authors = { workspace = true }
|
||||||
|
@ -9,7 +9,7 @@ description = "A very simple ebook server for a calibre library, providing a htm
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.4", features = ["http2", "tracing"] }
|
axum = { version = "0.8.4", features = ["http2", "tracing"] }
|
||||||
calibre-db = { path = "../calibre-db/", version = "0.1.0" }
|
calibre-db = { path = "../calibre-db/", version = "0.1.1" }
|
||||||
clap = { version = "4.5.40", features = ["derive", "env"] }
|
clap = { version = "4.5.40", features = ["derive", "env"] }
|
||||||
image = { version = "0.25.6", default-features = false, features = ["jpeg", "rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["jpeg", "rayon"] }
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Handlers for OPDS feeds.
|
//! Handlers for OPDS feeds.
|
||||||
pub mod authors;
|
pub mod authors;
|
||||||
pub mod books;
|
pub mod books;
|
||||||
|
pub mod feed;
|
||||||
pub mod recent;
|
pub mod recent;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod series;
|
pub mod series;
|
||||||
|
|
140
little-hesinde/src/api/opds/feed.rs
Normal file
140
little-hesinde/src/api/opds/feed.rs
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
use axum::{
|
||||||
|
http::{StatusCode, header},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use snafu::{ResultExt, Snafu};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
APP_NAME,
|
||||||
|
api::{
|
||||||
|
OPDS_TAG,
|
||||||
|
error::{ErrorResponse, HttpStatus},
|
||||||
|
},
|
||||||
|
http_error,
|
||||||
|
opds::{
|
||||||
|
content::Content, entry::Entry, error::AsXmlError, feed::Feed, link::Link,
|
||||||
|
media_type::MediaType, relation::Relation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
/// Errors that can occur when rendering the root OPDS feed.
|
||||||
|
pub enum OdpsFeedError {
|
||||||
|
/// A failure to create the OPDS feed.
|
||||||
|
#[snafu(display("Failed to create opds feed."))]
|
||||||
|
Feed { source: AsXmlError },
|
||||||
|
}
|
||||||
|
impl HttpStatus for OdpsFeedError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
OdpsFeedError::Feed { source: _ } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http_error!(OdpsFeedError);
|
||||||
|
|
||||||
|
/// Render all books as OPDS entries embedded in a feed.
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/",
|
||||||
|
tag = OPDS_TAG,
|
||||||
|
responses(
|
||||||
|
(status = OK, content_type = "application/atom+xml"),
|
||||||
|
(status = 500, description = "Server failure.", body = ErrorResponse)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub async fn handler() -> Result<Response, OdpsFeedError> {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
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: format!("{APP_NAME}:books"),
|
||||||
|
updated: now,
|
||||||
|
content: Some(Content {
|
||||||
|
media_type: MediaType::Text,
|
||||||
|
content: "Index of all books".to_string(),
|
||||||
|
}),
|
||||||
|
author: None,
|
||||||
|
links: vec![Link {
|
||||||
|
href: "/opds/books".to_string(),
|
||||||
|
media_type: MediaType::Navigation,
|
||||||
|
rel: Relation::Subsection,
|
||||||
|
title: None,
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let authors_entry = Entry {
|
||||||
|
title: "Authors".to_string(),
|
||||||
|
id: format!("{APP_NAME}:authors"),
|
||||||
|
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: format!("{APP_NAME}:series"),
|
||||||
|
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: format!("{APP_NAME}:recentbooks"),
|
||||||
|
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 = Feed::create(
|
||||||
|
now,
|
||||||
|
&format!("{APP_NAME}:catalog"),
|
||||||
|
"Little Hesinde",
|
||||||
|
self_link,
|
||||||
|
vec![],
|
||||||
|
vec![authors_entry, series_entry, books_entry, recents_entry],
|
||||||
|
);
|
||||||
|
let xml = feed.as_xml().context(FeedSnafu)?;
|
||||||
|
|
||||||
|
Ok(([(header::CONTENT_TYPE, "application/atom+xml")], xml).into_response())
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ pub fn router(state: AppState) -> OpenApiRouter {
|
||||||
let store = Arc::new(state);
|
let store = Arc::new(state);
|
||||||
|
|
||||||
let opds_routes = OpenApiRouter::new()
|
let opds_routes = OpenApiRouter::new()
|
||||||
|
.routes(routes!(opds::feed::handler))
|
||||||
.routes(routes!(opds::books::handler))
|
.routes(routes!(opds::books::handler))
|
||||||
.routes(routes!(opds::recent::handler))
|
.routes(routes!(opds::recent::handler))
|
||||||
.routes(routes!(opds::series::handler))
|
.routes(routes!(opds::series::handler))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue