bit of documentation

This commit is contained in:
Sebastian Hugentobler 2024-05-10 14:25:18 +02:00
parent a41dcab889
commit 870f457f1b
Signed by: shu
GPG key ID: BB32CF3CA052C2F0
47 changed files with 341 additions and 80 deletions

View file

@ -1,8 +1,13 @@
//! Data for global app state.
use calibre_db::calibre::Calibre;
use crate::config::Config;
/// Global application state, meant to be used in request handlers.
pub struct AppState {
/// Access calibre database.
pub calibre: Calibre,
/// Access application configuration.
pub config: Config,
}

View file

@ -1,44 +0,0 @@
use poem::{
http::StatusCode,
web::{
headers,
headers::{authorization::Basic, HeaderMapExt},
},
Endpoint, Error, Middleware, Request, Result,
};
pub struct BasicAuth {
pub username: String,
pub password: String,
}
impl<E: Endpoint> Middleware<E> for BasicAuth {
type Output = BasicAuthEndpoint<E>;
fn transform(&self, ep: E) -> Self::Output {
BasicAuthEndpoint {
ep,
username: self.username.clone(),
password: self.password.clone(),
}
}
}
pub struct BasicAuthEndpoint<E> {
ep: E,
username: String,
password: String,
}
impl<E: Endpoint> Endpoint for BasicAuthEndpoint<E> {
type Output = E::Output;
async fn call(&self, req: Request) -> Result<Self::Output> {
if let Some(auth) = req.headers().typed_get::<headers::Authorization<Basic>>() {
if auth.0.username() == self.username && auth.0.password() == self.password {
return self.ep.call(req).await;
}
}
Err(Error::from_status(StatusCode::UNAUTHORIZED))
}
}

View file

@ -1,3 +1,5 @@
//! Cli interface.
use clap::Parser;
/// Simple opds server for calibre

View file

@ -1,23 +1,32 @@
//! Configuration data.
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::cli::Cli;
/// Errors when dealing with application configuration.
#[derive(Error, Debug)]
pub enum ConfigError {
/// Calibre library path does not exist.
#[error("no folder at {0}")]
LibraryPathNotFound(String),
/// Calibre database does not exist.
#[error("no metadata.db in {0}")]
MetadataNotFound(String),
}
/// Application configuration.
pub struct Config {
/// Calibre library folder.
pub library_path: PathBuf,
/// Calibre metadata file path.
pub metadata_path: PathBuf,
}
impl Config {
/// Check if the calibre library from `args` exists and if the calibre database can be found.
pub fn load(args: &Cli) -> Result<Self, ConfigError> {
let library_path = Path::new(&args.library_path).to_path_buf();

View file

@ -1,17 +1,22 @@
//! Enrich the [`Book`](struct@calibre_db::data::book::Book) type with additional information.
use std::{collections::HashMap, fmt::Display, path::Path};
use calibre_db::data::{
author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries,
};
use serde::Serialize;
use time::OffsetDateTime;
use crate::app_state::AppState;
/// Wrapper type for a file format string (must be a struct in order to implement traits).
#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash)]
pub struct Format(pub String);
/// Wrapper type for a collection of formats, [`Format`](struct@Format) on the left and a String
/// signifying its file path on the right.
pub type Formats = HashMap<Format, String>;
/// Recognize `pdf` and `epub` and return their value, everything else transforms to `unknown`.
impl Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.as_ref() {
@ -22,21 +27,30 @@ impl Display for Format {
}
}
/// Wrapper around [`Book`](struct@calibre_db::data::book::Book) from the
/// [`calibre-db`](mod@calibre_db) crate. Adding
/// [`Author`](struct@calibre_db::data::author::Author),
/// [`Series`](struct@calibre_db::data::series::Series) as well as
/// [`Format`](struct@Format) information.
#[derive(Debug, Clone, Serialize)]
pub struct Book {
pub id: u64,
pub title: String,
pub sort: String,
pub path: String,
pub uuid: String,
pub last_modified: OffsetDateTime,
pub description: Option<String>,
/// Book data from the database.
pub data: DbBook,
/// Author information.
pub author: DbAuthor,
/// Series information. Series on the left and the index of the book within
/// that series on the right.
pub series: Option<(DbSeries, f64)>,
/// Format information.
pub formats: Formats,
}
impl Book {
/// Wrap a [`DbBook`](struct@calibre_db::data::book::Book) in a [`Book`](struct@Book) and
/// enrich it with additional information.
///
/// `db_series` is an Option tuple with the series on the left and the index of the book within
/// that series on the right.
pub fn from_db_book(
db_book: &DbBook,
db_series: Option<(DbSeries, f64)>,
@ -44,19 +58,15 @@ impl Book {
formats: Formats,
) -> Self {
Self {
id: db_book.id,
title: db_book.title.clone(),
sort: db_book.sort.clone(),
path: db_book.path.clone(),
uuid: db_book.uuid.clone(),
description: db_book.description.clone(),
last_modified: db_book.last_modified,
data: db_book.clone(),
author: author.clone(),
series: db_series.map(|x| (x.0, x.1)),
formats,
}
}
/// Find all pdf and epub formats of a book on disk. Search their directory in the library for
/// it.
fn formats(book: &DbBook, library_path: &Path) -> Formats {
let book_path = library_path.join(&book.path);
let mut formats = HashMap::new();
@ -80,6 +90,8 @@ impl Book {
formats
}
/// Wrap a [`DbBook`](struct@calibre_db::data::book::Book) in a [`Book`](struct@Book) by
/// fetching additional information about author, formats and series.
pub fn full_book(book: &DbBook, state: &AppState) -> Option<Book> {
let formats = Book::formats(book, &state.config.library_path);
let author = state.calibre.book_author(book.id).ok()?;

View file

@ -1,3 +1,5 @@
//! Handle requests for a single author.
use std::sync::Arc;
use calibre_db::data::pagination::SortOrder;
@ -9,6 +11,7 @@ use poem::{
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
/// Handle a request for an author with `id` and decide whether to render to html or OPDS.
#[handler]
pub async fn handler(
id: Path<u64>,

View file

@ -1,3 +1,5 @@
//! Handle requests for multiple authors.
use std::sync::Arc;
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
@ -9,6 +11,7 @@ use poem::{
use crate::{app_state::AppState, Accept};
/// Handle a request for multiple authors, starting at the first.
#[handler]
pub async fn handler_init(
accept: Data<&Accept>,
@ -17,6 +20,8 @@ pub async fn handler_init(
authors(&accept, &state.calibre, None, &SortOrder::ASC).await
}
/// Handle a request for multiple authors, starting at the `cursor` and going in the direction of
/// `sort_order`.
#[handler]
pub async fn handler(
Path((cursor, sort_order)): Path<(String, SortOrder)>,

View file

@ -1,3 +1,5 @@
//! Handle requests for multiple books.
use std::sync::Arc;
use calibre_db::data::pagination::SortOrder;
@ -9,6 +11,7 @@ use poem::{
use crate::{app_state::AppState, Accept};
/// Handle a request for multiple books, starting at the first.
#[handler]
pub async fn handler_init(
accept: Data<&Accept>,
@ -17,6 +20,8 @@ pub async fn handler_init(
books(&accept, &state, None, &SortOrder::ASC).await
}
/// Handle a request for multiple books, starting at the `cursor` and going in the direction of
/// `sort_order`.
#[handler]
pub async fn handler(
Path((cursor, sort_order)): Path<(String, SortOrder)>,

View file

@ -1,3 +1,5 @@
//! Handle requests for cover images.
use std::{fs::File, io::Read, sync::Arc};
use poem::{
@ -9,6 +11,7 @@ use poem::{
use crate::{app_state::AppState, handlers::error::HandlerError};
/// Handle a request for the cover image of book with id `id`.
#[handler]
pub async fn handler(
id: Path<u64>,

View file

@ -1,3 +1,5 @@
//! Handle requests for specific formats of a book.
use std::{fs::File, io::Read, sync::Arc};
use poem::{
@ -13,6 +15,7 @@ use crate::{
handlers::error::HandlerError,
};
/// Handle a request for a book with id `id` in format `format`.
#[handler]
pub async fn handler(
Path((id, format)): Path<(u64, String)>,
@ -25,7 +28,11 @@ pub async fn handler(
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
let format = Format(format);
let file_name = book.formats.get(&format).ok_or(NotFoundError)?;
let file_path = state.config.library_path.join(book.path).join(file_name);
let file_path = state
.config
.library_path
.join(book.data.path)
.join(file_name);
let mut file = File::open(file_path).map_err(|_| NotFoundError)?;
let mut data = Vec::new();

View file

@ -1,3 +1,5 @@
//! Error handling for requests handlers.
use calibre_db::data::error::DataStoreError;
use poem::{error::ResponseError, http::StatusCode, Body, Response};
use thiserror::Error;
@ -6,15 +8,22 @@ use uuid::Uuid;
use crate::opds::error::OpdsError;
/// Errors happening during handling of requests.
#[derive(Error, Debug)]
#[error("opds error")]
pub enum HandlerError {
/// Error rendering OPDS.
#[error("opds error")]
OpdsError(#[from] OpdsError),
/// Error fetching data from calibre.
#[error("data error")]
DataError(#[from] DataStoreError),
}
/// Convert a [`HandlerError`](enum@HandlerError) into a suitable response error.
///
/// Log the real error (internal) with an uuid and send a suitable error message to the user with
/// the same uuid (for correlation purposes).
impl ResponseError for HandlerError {
fn status(&self) -> StatusCode {
match &self {

View file

@ -1,9 +1,12 @@
//! Handle a single author for html.
use calibre_db::data::author::Author;
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
use tera::Context;
use crate::{data::book::Book, templates::TEMPLATES};
/// Render a single author in html.
pub async fn handler(author: Author, books: Vec<Book>) -> Result<Response, poem::Error> {
let mut context = Context::new();
context.insert("title", &author.name);

View file

@ -1,8 +1,11 @@
//! Handle multiple authors in html.
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
use poem::Response;
use crate::handlers::paginated;
/// Render all authors paginated by cursor in html.
pub async fn handler(
calibre: &Calibre,
cursor: Option<&str>,

View file

@ -1,8 +1,11 @@
//! Handle multiple books in html.
use calibre_db::data::pagination::SortOrder;
use poem::Response;
use crate::{app_state::AppState, data::book::Book, handlers::paginated};
/// Render all books paginated by cursor in html.
pub async fn handler(
state: &AppState,
cursor: Option<&str>,
@ -16,7 +19,7 @@ pub async fn handler(
.books(25, cursor, sort_order)
.map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect())
},
|book| book.sort.clone(),
|book| book.data.sort.clone(),
|cursor| state.calibre.has_previous_books(cursor),
|cursor| state.calibre.has_more_books(cursor),
)

View file

@ -1,8 +1,11 @@
//! Handle recent books in html.
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
use tera::Context;
use crate::{data::book::Book, templates::TEMPLATES};
/// Render recent books as html.
pub async fn handler(recent_books: Vec<Book>) -> Result<Response, poem::Error> {
let mut context = Context::new();
context.insert("title", "Recent Books");

View file

@ -1,8 +1,11 @@
//! Handle multiple series in html.
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
use poem::Response;
use crate::handlers::paginated;
/// Render all series paginated by cursor as html.
pub async fn handler(
calibre: &Calibre,
cursor: Option<&str>,

View file

@ -1,9 +1,12 @@
//! Handle a single series in html.
use calibre_db::data::series::Series;
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
use tera::Context;
use crate::{data::book::Book, templates::TEMPLATES};
/// Render a single series as html.
pub async fn handler(series: Series, books: Vec<Book>) -> Result<Response, poem::Error> {
let mut context = Context::new();
context.insert("title", &series.name);

View file

@ -1,3 +1,5 @@
//! Handle a single author for opds.
use calibre_db::data::author::Author;
use poem::{IntoResponse, Response};
use time::OffsetDateTime;
@ -8,6 +10,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render a single author as an OPDS entry embedded in a feed.
pub async fn handler(author: Author, books: Vec<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = books.into_iter().map(Entry::from).collect();
let now = OffsetDateTime::now_utc();

View file

@ -1,3 +1,5 @@
//! Handle multiple authors for opds.
use calibre_db::{
calibre::Calibre,
data::{author::Author as DbAuthor, pagination::SortOrder},
@ -10,6 +12,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render all authors as OPDS entries embedded in a feed.
pub async fn handler(
calibre: &Calibre,
_cursor: Option<&str>,

View file

@ -1,3 +1,5 @@
//! Handle multiple books for opds.
use calibre_db::data::pagination::SortOrder;
use poem::{IntoResponse, Response};
use time::OffsetDateTime;
@ -9,6 +11,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render all books as OPDS entries embedded in a feed.
pub async fn handler(
state: &AppState,
_cursor: Option<&str>,

View file

@ -1,3 +1,5 @@
//! Handle the OPDS root feed.
use poem::{handler, web::WithContentType, IntoResponse};
use time::OffsetDateTime;
@ -9,6 +11,7 @@ use crate::{
},
};
/// Render a root OPDS feed with links to the subsections (authors, books, series and recent).
#[handler]
pub async fn handler() -> Result<WithContentType<String>, poem::Error> {
let now = OffsetDateTime::now_utc();

View file

@ -1,3 +1,5 @@
//! Handle recent books for OPDS.
use poem::{IntoResponse, Response};
use time::OffsetDateTime;
@ -7,6 +9,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render recent books as OPDS entries embedded in a feed.
pub async fn handler(recent_books: Vec<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = recent_books.into_iter().map(Entry::from).collect();
let now = OffsetDateTime::now_utc();

View file

@ -1,3 +1,5 @@
//! Handle multiple series for OPDS.
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
use poem::{IntoResponse, Response};
use time::OffsetDateTime;
@ -7,6 +9,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render all series as OPDS entries embedded in a feed.
pub async fn handler(
calibre: &Calibre,
_cursor: Option<&str>,

View file

@ -1,3 +1,5 @@
//! Handle a single series for opds.
use calibre_db::data::series::Series;
use poem::{IntoResponse, Response};
use time::OffsetDateTime;
@ -8,6 +10,7 @@ use crate::{
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
};
/// Render a single series as an OPDS entry embedded in a feed.
pub async fn handler(series: Series, books: Vec<Book>) -> Result<Response, poem::Error> {
let entries: Vec<Entry> = books.into_iter().map(Entry::from).collect();
let now = OffsetDateTime::now_utc();

View file

@ -1,3 +1,5 @@
//! Deal with cursor pagination.
use std::fmt::Debug;
use calibre_db::data::error::DataStoreError;
@ -9,6 +11,7 @@ use crate::templates::TEMPLATES;
use super::error::HandlerError;
/// Render a tera template with paginated items and generate back and forth links.
pub fn render<T: Serialize + Debug, F, S, P, M>(
template: &str,
fetcher: F,

View file

@ -1,9 +1,12 @@
//! Handle requests for recent books.
use std::sync::Arc;
use poem::{handler, web::Data, Response};
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
/// Handle a request recent books and decide whether to render to html or OPDS.
#[handler]
pub async fn handler(
accept: Data<&Accept>,

View file

@ -1,3 +1,5 @@
//! Handle requests for multiple series.
use std::sync::Arc;
use calibre_db::data::pagination::SortOrder;
@ -9,6 +11,7 @@ use poem::{
use crate::{app_state::AppState, Accept};
/// Handle a request for multiple series, starting at the first.
#[handler]
pub async fn handler_init(
accept: Data<&Accept>,
@ -17,6 +20,8 @@ pub async fn handler_init(
series(&accept, &state, None, &SortOrder::ASC).await
}
/// Handle a request for multiple series, starting at the `cursor` and going in the direction of
/// `sort_order`.
#[handler]
pub async fn handler(
Path((cursor, sort_order)): Path<(String, SortOrder)>,

View file

@ -1,3 +1,5 @@
//! Handle requests for a single series.
use std::sync::Arc;
use poem::{
@ -8,6 +10,7 @@ use poem::{
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
/// Handle a request for a series with `id` and decide whether to render to html or OPDS.
#[handler]
pub async fn handler(
id: Path<u64>,

View file

@ -1,3 +1,7 @@
//! A very simple ebook server for a calibre library, providing a html interface as well as an OPDS feed.
//!
//! Shamelessly written to scratch my own itches.
use std::sync::Arc;
use app_state::AppState;
@ -10,13 +14,17 @@ use poem::{
use rust_embed::RustEmbed;
pub mod app_state;
pub mod basic_auth;
pub mod cli;
pub mod config;
/// Data structs and their functions.
pub mod data {
pub mod book;
}
/// Request handlers. Because it can not be guaranteed that a proper accept header is sent, the
/// routes are doubled and the decision on whether to render html or OPDS is made with internal
/// data on the respective routes.
pub mod handlers {
/// Handle requests for html.
pub mod html {
pub mod author;
pub mod authors;
@ -25,6 +33,7 @@ pub mod handlers {
pub mod series;
pub mod series_single;
}
/// Handle requests for OPDS.
pub mod opds {
pub mod author;
pub mod authors;
@ -45,19 +54,34 @@ pub mod handlers {
pub mod series;
pub mod series_single;
}
pub mod opds;
/// OPDS data structs.
pub mod opds {
pub mod author;
pub mod content;
pub mod entry;
pub mod error;
pub mod feed;
pub mod link;
pub mod media_type;
pub mod relation;
}
pub mod templates;
/// Internal marker data in lieu of a proper `Accept` header.
#[derive(Debug, Clone, Copy)]
pub enum Accept {
/// Render as html.
Html,
/// Render as OPDS.
Opds,
}
/// Embedd static files.
#[derive(RustEmbed)]
#[folder = "static"]
pub struct Files;
/// Main entry point to run the ebook server with a calibre library specified in `config`.
pub async fn run(config: Config) -> Result<(), std::io::Error> {
let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database");
let app_state = Arc::new(AppState { calibre, config });

View file

@ -1,10 +1,16 @@
//! Author data.
use serde::Serialize;
/// Author information.
#[derive(Debug, Serialize)]
#[serde(rename = "author")]
pub struct Author {
/// Full name.
pub name: String,
/// Where to find the author.
pub uri: String,
/// Optional email address.
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
}

View file

@ -1,12 +1,17 @@
//! Content data.
use serde::Serialize;
use super::media_type::MediaType;
/// Content of different types, used for example for description of an entry.
#[derive(Debug, Serialize)]
#[serde(rename = "content")]
pub struct Content {
/// Media type of this content.
#[serde(rename = "@type")]
pub media_type: MediaType,
/// Actual content.
#[serde(rename = "$value")]
pub content: String,
}

View file

@ -1,3 +1,5 @@
//! Entry data.
use calibre_db::data::{author::Author as DbAuthor, series::Series};
use serde::Serialize;
use time::OffsetDateTime;
@ -8,21 +10,32 @@ use super::{
author::Author, content::Content, link::Link, media_type::MediaType, relation::Relation,
};
/// Fundamental piece of OPDS, holding information about entries (for example a book).
#[derive(Debug, Serialize)]
#[serde(rename = "entry")]
pub struct Entry {
/// Title of the entry.
pub title: String,
/// Id, for example a uuid.
pub id: String,
/// When was this entry updated last.
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
/// Optional content.
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional author information.
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<Author>,
/// List of links, for example to download an entry.
#[serde(rename = "link")]
pub links: Vec<Link>,
}
/// Convert a book into an OPDS entry.
///
/// Add the cover and formats as link with a proper media type.
/// Add author and content information.
impl From<Book> for Entry {
fn from(value: Book) -> Self {
let author = Author {
@ -31,7 +44,7 @@ impl From<Book> for Entry {
email: None,
};
let mut links = vec![Link {
href: format!("/cover/{}", value.id),
href: format!("/cover/{}", value.data.id),
media_type: MediaType::Jpeg,
rel: Relation::Image,
title: None,
@ -44,15 +57,15 @@ impl From<Book> for Entry {
.collect();
links.append(&mut format_links);
let content = value.description.map(|desc| Content {
let content = value.data.description.map(|desc| Content {
media_type: MediaType::Html,
content: desc,
});
Self {
title: value.title.clone(),
id: format!("urn:uuid:{}", value.uuid),
updated: value.last_modified,
title: value.data.title.clone(),
id: format!("urn:uuid:{}", value.data.uuid),
updated: value.data.last_modified,
content,
author: Some(author),
links,
@ -60,6 +73,9 @@ impl From<Book> for Entry {
}
}
/// Convert author information into an OPDS entry.
///
/// Add the author link.
impl From<DbAuthor> for Entry {
fn from(value: DbAuthor) -> Self {
let links = vec![Link {
@ -81,6 +97,9 @@ impl From<DbAuthor> for Entry {
}
}
/// Convert series information into an OPDS entry.
///
/// Add the series link.
impl From<Series> for Entry {
fn from(value: Series) -> Self {
let links = vec![Link {

View file

@ -1,15 +1,21 @@
//! Error handling for OPDS data.
use std::string::FromUtf8Error;
use quick_xml::DeError;
use thiserror::Error;
/// Errors happening during handling OPDS data.
#[derive(Error, Debug)]
#[error("opds error")]
pub enum OpdsError {
/// Error serializing OPDS data.
#[error("failed to serialize struct")]
SerializingError(#[from] DeError),
/// Error parsing OPDS xml structure.
#[error("xml failure")]
XmlError(#[from] quick_xml::Error),
/// Error decoding xml as UTF-8.
#[error("failed to decode as utf-8")]
Utf8Error(#[from] FromUtf8Error),
}

View file

@ -1,3 +1,5 @@
//! Root feed data.
use std::io::Cursor;
use quick_xml::{
@ -13,22 +15,31 @@ use super::{
relation::Relation,
};
/// Root feed element of OPDS.
#[derive(Debug, Serialize)]
#[serde(rename = "feed")]
pub struct Feed {
/// Title, often shown in OPDS clients.
pub title: String,
/// Feed id.
pub id: String,
/// When was the feed updated last.
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
/// Icon for the feed.
pub icon: String,
/// Feed author.
pub author: Author,
/// Links, for example home or self.
#[serde(rename = "link")]
pub links: Vec<Link>,
/// Entries inside the feed (books, series, subsections, ...)
#[serde(rename = "entry")]
pub entries: Vec<Entry>,
}
impl Feed {
/// Create a feed with the specified data.
pub fn create(
now: OffsetDateTime,
id: &str,
@ -65,6 +76,7 @@ impl Feed {
}
}
/// Serialize a feed to OPDS xml.
pub fn as_xml(&self) -> Result<String, OpdsError> {
let xml = to_string(&self)?;
let mut reader = Reader::from_str(&xml);

View file

@ -1,32 +1,41 @@
//! Link data.
use serde::Serialize;
use crate::data::book::{Book, Format};
use super::{media_type::MediaType, relation::Relation};
/// Link element in OPDS.
#[derive(Debug, Serialize)]
#[serde(rename = "link")]
pub struct Link {
/// Actual hyperlink.
#[serde(rename = "@href")]
pub href: String,
/// Type of the target.
#[serde(rename = "@type")]
pub media_type: MediaType,
/// Relation of the target.
#[serde(rename = "@rel")]
pub rel: Relation,
/// Optional link title.
#[serde(rename = "@title")]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Optional count (how many entries at the target).
#[serde(rename = "@thr:count")]
#[serde(skip_serializing_if = "Option::is_none")]
pub count: Option<u64>,
}
/// Convert a format from a book into a link where it is downloadable.
impl From<(&Book, (&Format, &str))> for Link {
fn from(value: (&Book, (&Format, &str))) -> Self {
let format = value.1 .0.clone();
let media_type: MediaType = format.into();
Self {
href: format!("/book/{}/{}", value.0.id, value.1 .0),
href: format!("/book/{}/{}", value.0.data.id, value.1 .0),
media_type,
rel: media_type.into(),
title: Some(value.1 .0 .0.clone()),

View file

@ -1,18 +1,24 @@
//! Media types for OPDS elements.
use serde_with::SerializeDisplay;
use crate::data::book::Format;
/// Valid media types.
#[derive(Debug, Copy, Clone, SerializeDisplay)]
pub enum MediaType {
/// A link with this type is meant to acquire a certain thing, for example an entry.
Acquisition,
Epub,
Html,
Jpeg,
/// A link with this type is meant for navigation around a feed.
Navigation,
Pdf,
Text,
}
/// Convert `epub` and `pdf` formats to their respective media type. Everything else is `Text`.
impl From<Format> for MediaType {
fn from(value: Format) -> Self {
match value.0.as_ref() {
@ -23,6 +29,7 @@ impl From<Format> for MediaType {
}
}
/// Display the respective mime types of the respective media types.
impl std::fmt::Display for MediaType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -1,8 +0,0 @@
pub mod author;
pub mod content;
pub mod entry;
pub mod error;
pub mod feed;
pub mod link;
pub mod media_type;
pub mod relation;

View file

@ -1,10 +1,14 @@
//! Relation data.
use serde_with::SerializeDisplay;
use super::media_type::MediaType;
/// Types of relations for links.
#[derive(Debug, SerializeDisplay)]
pub enum Relation {
Image,
/// Refer to the self feed.
Myself,
Start,
Subsection,
@ -12,6 +16,9 @@ pub enum Relation {
Acquisition,
}
/// Convert a media type int a relation.
///
/// This is not always deterministic but for the ones I actually use so far it is correct.
impl From<MediaType> for Relation {
fn from(value: MediaType) -> Self {
match value {
@ -26,6 +33,7 @@ impl From<MediaType> for Relation {
}
}
/// Specify how to represent all relations in OPDS.
impl std::fmt::Display for Relation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -1,6 +1,9 @@
//! Tera templates.
use once_cell::sync::Lazy;
use tera::Tera;
/// All tera templates, globally accessible.
pub static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![

View file

@ -27,7 +27,11 @@
</main>
<footer class="container">
<hr />
<small>From <a href="https://code.vanwa.ch/shu/rusty-library">https://code.vanwa.ch</a> under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL-3</a></small>
<small>
From <a href="https://code.vanwa.ch/shu/rusty-library">https://code.vanwa.ch</a>
under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL-3</a>
- <a href="/opds">opds feed</a>
</small>
</footer>
</body>
</html>