Compare commits

...

2 commits

Author SHA1 Message Date
4ab8c42f69
code formatting 2025-07-03 19:31:15 +02:00
491598aaaf
add lost opds root feed 2025-07-03 19:30:56 +02:00
11 changed files with 152 additions and 13 deletions

4
Cargo.lock generated
View file

@ -305,7 +305,7 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "calibre-db"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"r2d2",
"r2d2_sqlite",
@ -1222,7 +1222,7 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "little-hesinde"
version = "0.3.4"
version = "0.3.5"
dependencies = [
"axum",
"calibre-db",

View file

@ -1,6 +1,6 @@
[package]
name = "calibre-db"
version = "0.1.0"
version = "0.1.1"
edition = "2024"
license = { workspace = true }
authors = { workspace = true }

View file

@ -1,6 +1,6 @@
[package]
name = "little-hesinde"
version = "0.3.4"
version = "0.3.5"
edition = "2024"
license = { workspace = true }
authors = { workspace = true }
@ -9,7 +9,7 @@ description = "A very simple ebook server for a calibre library, providing a htm
[dependencies]
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"] }
image = { version = "0.25.6", default-features = false, features = ["jpeg", "rayon"] }
mime_guess = "2.0.5"

View file

@ -1,6 +1,7 @@
//! Handlers for OPDS feeds.
pub mod authors;
pub mod books;
pub mod feed;
pub mod recent;
pub mod search;
pub mod series;

View file

@ -12,10 +12,9 @@ use time::OffsetDateTime;
use crate::{
APP_NAME,
api::{
SortOrder,
OPDS_TAG, SortOrder,
authors::{self, SingleAuthorError},
error::{ErrorResponse, HttpStatus},
OPDS_TAG,
},
app_state::AppState,
http_error,

View file

@ -12,9 +12,8 @@ use time::OffsetDateTime;
use crate::{
APP_NAME,
api::{
SortOrder,
OPDS_TAG, SortOrder,
error::{ErrorResponse, HttpStatus},
OPDS_TAG,
},
app_state::AppState,
data::book::Book,

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

View file

@ -11,9 +11,9 @@ use time::OffsetDateTime;
use crate::{
APP_NAME,
api::{
OPDS_TAG,
books::{self, RecentBooksError},
error::{ErrorResponse, HttpStatus},
OPDS_TAG,
},
app_state::AppState,
http_error,

View file

@ -12,9 +12,9 @@ use super::books::{RenderError, render_books};
use crate::{
APP_NAME,
api::{
OPDS_TAG,
error::{ErrorResponse, HttpStatus},
search::{self, SearchQueryError},
OPDS_TAG,
},
app_state::AppState,
http_error,

View file

@ -12,10 +12,9 @@ use time::OffsetDateTime;
use crate::{
APP_NAME,
api::{
SortOrder,
OPDS_TAG, SortOrder,
error::{ErrorResponse, HttpStatus},
series::{self, SingleSeriesError},
OPDS_TAG,
},
app_state::AppState,
http_error,

View file

@ -16,6 +16,7 @@ pub fn router(state: AppState) -> OpenApiRouter {
let store = Arc::new(state);
let opds_routes = OpenApiRouter::new()
.routes(routes!(opds::feed::handler))
.routes(routes!(opds::books::handler))
.routes(routes!(opds::recent::handler))
.routes(routes!(opds::series::handler))