diff --git a/calibre-db/src/calibre.rs b/calibre-db/src/calibre.rs index 221a4cb..fc6bc53 100644 --- a/calibre-db/src/calibre.rs +++ b/calibre-db/src/calibre.rs @@ -1,3 +1,5 @@ +//! Bundle all functions together. + use std::path::Path; use r2d2::Pool; @@ -7,12 +9,17 @@ use crate::data::{ author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series, }; +/// Top level calibre functions, bundling all sub functions in one place and providing secure access to +/// the database. #[derive(Debug, Clone)] pub struct Calibre { pool: Pool, } impl Calibre { + /// Open a connection to the calibre database. + /// + /// Fail if the database file can not be opened or not be found. pub fn load(path: &Path) -> Result { let manager = SqliteConnectionManager::file(path); let pool = r2d2::Pool::new(manager)?; @@ -20,6 +27,8 @@ impl Calibre { Ok(Self { pool }) } + /// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn books( &self, limit: u64, @@ -30,6 +39,8 @@ impl Calibre { Book::multiple(&conn, limit, cursor, sort_order) } + /// Fetch author data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn authors( &self, limit: u64, @@ -40,6 +51,8 @@ impl Calibre { Author::multiple(&conn, limit, cursor, sort_order) } + /// Fetch books for an author specified by `author_id`, paginate the books by starting at `cursor`, + /// fetching up to an amount of `limit` and ordering by `sort_order`. pub fn author_books( &self, author_id: u64, @@ -51,21 +64,26 @@ impl Calibre { Book::author_books(&conn, author_id, limit, cursor, sort_order) } + /// Get recent books up to a limit of `limit`. pub fn recent_books(&self, limit: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Book::recents(&conn, limit) } + /// Get a single book, specified `id`. pub fn scalar_book(&self, id: u64) -> Result { let conn = self.pool.get()?; Book::scalar_book(&conn, id) } + /// Get the author to a book with id `id`. pub fn book_author(&self, id: u64) -> Result { let conn = self.pool.get()?; Author::book_author(&conn, id) } + /// Fetch series data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn series( &self, limit: u64, @@ -76,51 +94,61 @@ impl Calibre { Series::multiple(&conn, limit, cursor, sort_order) } + /// Get the series a book with id `id` is in, as well as the book's position within the series. pub fn book_series(&self, id: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Series::book_series(&conn, id) } + /// Get all books belonging to the series with id `id`. pub fn series_books(&self, id: u64) -> Result, DataStoreError> { let conn = self.pool.get()?; Book::series_books(&conn, id) } + /// Check if there are more authors before the specified cursor. pub fn has_previous_authors(&self, author_sort: &str) -> Result { let conn = self.pool.get()?; Author::has_previous_authors(&conn, author_sort) } + /// Check if there are more authors after the specified cursor. pub fn has_more_authors(&self, author_sort: &str) -> Result { let conn = self.pool.get()?; Author::has_more_authors(&conn, author_sort) } + /// Check if there are more books before the specified cursor. pub fn has_previous_books(&self, book_sort: &str) -> Result { let conn = self.pool.get()?; Book::has_previous_books(&conn, book_sort) } + /// Check if there are more books after the specified cursor. pub fn has_more_books(&self, book_sort: &str) -> Result { let conn = self.pool.get()?; Book::has_more_books(&conn, book_sort) } + /// Check if there are more series before the specified cursor. pub fn has_previous_series(&self, series_sort: &str) -> Result { let conn = self.pool.get()?; Series::has_previous_series(&conn, series_sort) } + /// Check if there are more series after the specified cursor. pub fn has_more_series(&self, series_sort: &str) -> Result { let conn = self.pool.get()?; Series::has_more_series(&conn, series_sort) } + /// Fetch a single author with id `id`. pub fn scalar_author(&self, id: u64) -> Result { let conn = self.pool.get()?; Author::scalar_author(&conn, id) } + /// Fetch a single series with id `id`. pub fn scalar_series(&self, id: u64) -> Result { let conn = self.pool.get()?; Series::scalar_series(&conn, id) diff --git a/calibre-db/src/data/author.rs b/calibre-db/src/data/author.rs index 2ecf9e5..e79da5a 100644 --- a/calibre-db/src/data/author.rs +++ b/calibre-db/src/data/author.rs @@ -1,3 +1,5 @@ +//! Author data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; @@ -6,10 +8,14 @@ use super::{ pagination::{Pagination, SortOrder}, }; +/// Author in calibre. #[derive(Debug, Clone, Serialize)] pub struct Author { + /// Id in database. pub id: u64, + /// Full name. pub name: String, + /// Full name for sorting. pub sort: String, } @@ -22,6 +28,8 @@ impl Author { }) } + /// Fetch author data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -37,6 +45,7 @@ impl Author { ) } + /// Get the author to a book with id `id`. pub fn book_author(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare( "SELECT authors.id, authors.name, authors.sort FROM authors \ @@ -47,12 +56,14 @@ impl Author { Ok(stmt.query_row(params, Self::from_row)?) } + /// Fetch a single author with id `id`. pub fn scalar_author(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare("SELECT id, name, sort FROM authors WHERE id = (:id)")?; let params = named_params! { ":id": id }; Ok(stmt.query_row(params, Self::from_row)?) } + /// Check if there are more authors before the specified cursor. pub fn has_previous_authors( conn: &Connection, sort_name: &str, @@ -60,6 +71,7 @@ impl Author { Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::DESC) } + /// Check if there are more authors after the specified cursor. pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::ASC) } diff --git a/calibre-db/src/data/book.rs b/calibre-db/src/data/book.rs index a893b2e..4547008 100644 --- a/calibre-db/src/data/book.rs +++ b/calibre-db/src/data/book.rs @@ -1,3 +1,5 @@ +//! Book data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; use time::OffsetDateTime; @@ -7,14 +9,22 @@ use super::{ pagination::{Pagination, SortOrder}, }; -#[derive(Debug, Serialize)] +/// Book in calibre. +#[derive(Debug, Clone, Serialize)] pub struct Book { + /// Id in database. pub id: u64, + /// Book title. pub title: String, + /// Book title for sorting. pub sort: String, + /// Folder of the book within the calibre library. pub path: String, + /// Uuid of the book. pub uuid: String, + /// When was the book last modified. pub last_modified: OffsetDateTime, + /// Optional description. pub description: Option, } @@ -31,6 +41,8 @@ impl Book { }) } + /// Fetch book data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -47,6 +59,8 @@ impl Book { ) } + /// Fetch books for an author specified by `author_id`, paginate the books by starting at `cursor`, + /// fetching up to an amount of `limit` and ordering by `sort_order`. pub fn author_books( conn: &Connection, author_id: u64, @@ -66,6 +80,7 @@ impl Book { ) } + /// Get all books belonging to the series with id `id`. pub fn series_books(conn: &Connection, id: u64) -> Result, DataStoreError> { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text FROM series \ @@ -80,6 +95,7 @@ impl Book { Ok(iter.filter_map(Result::ok).collect()) } + /// Get recent books up to a limit of `limit`. pub fn recents(conn: &Connection, limit: u64) -> Result, DataStoreError> { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \ @@ -90,6 +106,7 @@ impl Book { Ok(iter.filter_map(Result::ok).collect()) } + /// Get a single book, specified `id`. pub fn scalar_book(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare( "SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \ @@ -99,10 +116,12 @@ impl Book { Ok(stmt.query_row(params, Self::from_row)?) } + /// Check if there are more books before the specified cursor. pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result { Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::DESC) } + /// Check if there are more books after the specified cursor. pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result { Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC) } diff --git a/calibre-db/src/data/error.rs b/calibre-db/src/data/error.rs index 0680abc..ea78a79 100644 --- a/calibre-db/src/data/error.rs +++ b/calibre-db/src/data/error.rs @@ -1,19 +1,28 @@ +//! Error handling for calibre database access. + use thiserror::Error; use time::error::Parse; +/// Errors from accessing the calibre database. #[derive(Error, Debug)] #[error("data store error")] pub enum DataStoreError { + /// Found no entries for the query. #[error("no results")] NoResults(rusqlite::Error), + /// Error with SQLite. #[error("sqlite error")] SqliteError(rusqlite::Error), + /// Error connecting to the database. #[error("connection error")] ConnectionError(#[from] r2d2::Error), + /// Error wparsing a datetime from the database. #[error("failed to parse datetime")] DateTimeError(#[from] Parse), } +/// Convert an SQLite error into a proper NoResults one if the query +/// returned no rows, return others as is. impl From for DataStoreError { fn from(error: rusqlite::Error) -> Self { match error { diff --git a/calibre-db/src/data/pagination.rs b/calibre-db/src/data/pagination.rs index d08449b..c2f6064 100644 --- a/calibre-db/src/data/pagination.rs +++ b/calibre-db/src/data/pagination.rs @@ -1,22 +1,33 @@ +//! Cursor pagination handling. + use rusqlite::{named_params, Connection, Row, ToSql}; use serde::{Deserialize, Serialize}; use super::error::DataStoreError; +/// How to sort query results. Signifying whether we are paginating forwards or backwards. #[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] pub enum SortOrder { + /// Forwards ASC, + /// Backwards DESC, } +/// Pagination data. pub struct Pagination<'a> { + /// Sort by this column. pub sort_col: &'a str, + /// Limit returned results. pub limit: u64, + /// Where to start paginating. pub cursor: Option<&'a str>, + /// Paginating forwards or backwards. pub sort_order: SortOrder, } impl<'a> Pagination<'a> { + /// Create a new pagination. pub fn new( sort_col: &'a str, cursor: Option<&'a str>, @@ -40,6 +51,7 @@ impl<'a> Pagination<'a> { .to_string() } + /// Check if there are more items forwards or backwards from `cursor` (direction specified by `sort_order`). pub fn has_prev_or_more( conn: &Connection, table: &str, @@ -57,6 +69,7 @@ impl<'a> Pagination<'a> { Ok(count > 0) } + /// Paginate a statement. pub fn paginate( &self, conn: &Connection, @@ -77,7 +90,8 @@ impl<'a> Pagination<'a> { }; let sort_col = self.sort_col; - // otherwise paginated statements with join will fail + // otherwise paginated statements with join will fails, not happy with this but fine for + // now let sort_col_wrapped = if let Some(index) = sort_col.find('.') { let right_part = &sort_col[index..]; "t".to_owned() + right_part diff --git a/calibre-db/src/data/series.rs b/calibre-db/src/data/series.rs index ffe43bf..b467fde 100644 --- a/calibre-db/src/data/series.rs +++ b/calibre-db/src/data/series.rs @@ -1,3 +1,5 @@ +//! Series data. + use rusqlite::{named_params, Connection, Row}; use serde::Serialize; @@ -6,10 +8,14 @@ use super::{ pagination::{Pagination, SortOrder}, }; +/// Series in calibre. #[derive(Debug, Clone, Serialize)] pub struct Series { + /// Id in database. pub id: u64, + /// Series name. pub name: String, + /// Series name for sorting. pub sort: String, } @@ -22,6 +28,8 @@ impl Series { }) } + /// Fetch series data from calibre, starting at `cursor`, fetching up to an amount of `limit` and + /// ordering by `sort_order`. pub fn multiple( conn: &Connection, limit: u64, @@ -37,12 +45,14 @@ impl Series { ) } + /// Fetch a single series with id `id`. pub fn scalar_series(conn: &Connection, id: u64) -> Result { let mut stmt = conn.prepare("SELECT id, name, sort FROM series WHERE id = (:id)")?; let params = named_params! { ":id": id }; Ok(stmt.query_row(params, Self::from_row)?) } + /// Get the series a book with id `id` is in, as well as the book's position within the series. pub fn book_series( conn: &Connection, book_id: u64, @@ -68,10 +78,12 @@ impl Series { } } + /// Check if there are more series before the specified cursor. pub fn has_previous_series(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::DESC) } + /// Check if there are more series after the specified cursor. pub fn has_more_series(conn: &Connection, sort_name: &str) -> Result { Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::ASC) } diff --git a/calibre-db/src/lib.rs b/calibre-db/src/lib.rs index c7a0452..bae8fbc 100644 --- a/calibre-db/src/lib.rs +++ b/calibre-db/src/lib.rs @@ -1,4 +1,7 @@ +//! Read data from a calibre library, leveraging its SQLite metadata database. + pub mod calibre; +/// Data structs for the calibre database. pub mod data { pub mod author; pub mod book; diff --git a/rusty-library/src/app_state.rs b/rusty-library/src/app_state.rs index 9b886cc..7136258 100644 --- a/rusty-library/src/app_state.rs +++ b/rusty-library/src/app_state.rs @@ -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, } diff --git a/rusty-library/src/basic_auth.rs b/rusty-library/src/basic_auth.rs deleted file mode 100644 index 092bde4..0000000 --- a/rusty-library/src/basic_auth.rs +++ /dev/null @@ -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 Middleware for BasicAuth { - type Output = BasicAuthEndpoint; - - fn transform(&self, ep: E) -> Self::Output { - BasicAuthEndpoint { - ep, - username: self.username.clone(), - password: self.password.clone(), - } - } -} - -pub struct BasicAuthEndpoint { - ep: E, - username: String, - password: String, -} - -impl Endpoint for BasicAuthEndpoint { - type Output = E::Output; - - async fn call(&self, req: Request) -> Result { - if let Some(auth) = req.headers().typed_get::>() { - if auth.0.username() == self.username && auth.0.password() == self.password { - return self.ep.call(req).await; - } - } - Err(Error::from_status(StatusCode::UNAUTHORIZED)) - } -} diff --git a/rusty-library/src/cli.rs b/rusty-library/src/cli.rs index 5a7f28c..6f1c404 100644 --- a/rusty-library/src/cli.rs +++ b/rusty-library/src/cli.rs @@ -1,3 +1,5 @@ +//! Cli interface. + use clap::Parser; /// Simple opds server for calibre diff --git a/rusty-library/src/config.rs b/rusty-library/src/config.rs index 0747cf3..9d0b97e 100644 --- a/rusty-library/src/config.rs +++ b/rusty-library/src/config.rs @@ -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 { let library_path = Path::new(&args.library_path).to_path_buf(); diff --git a/rusty-library/src/data/book.rs b/rusty-library/src/data/book.rs index ac5fc12..849144e 100644 --- a/rusty-library/src/data/book.rs +++ b/rusty-library/src/data/book.rs @@ -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; +/// 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, + /// 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 { let formats = Book::formats(book, &state.config.library_path); let author = state.calibre.book_author(book.id).ok()?; diff --git a/rusty-library/src/handlers/author.rs b/rusty-library/src/handlers/author.rs index 5617182..60f7e20 100644 --- a/rusty-library/src/handlers/author.rs +++ b/rusty-library/src/handlers/author.rs @@ -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, diff --git a/rusty-library/src/handlers/authors.rs b/rusty-library/src/handlers/authors.rs index 28f9e19..f6d4ba1 100644 --- a/rusty-library/src/handlers/authors.rs +++ b/rusty-library/src/handlers/authors.rs @@ -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)>, diff --git a/rusty-library/src/handlers/books.rs b/rusty-library/src/handlers/books.rs index b481a2b..9944821 100644 --- a/rusty-library/src/handlers/books.rs +++ b/rusty-library/src/handlers/books.rs @@ -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)>, diff --git a/rusty-library/src/handlers/cover.rs b/rusty-library/src/handlers/cover.rs index 022cb3d..4a0696d 100644 --- a/rusty-library/src/handlers/cover.rs +++ b/rusty-library/src/handlers/cover.rs @@ -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, diff --git a/rusty-library/src/handlers/download.rs b/rusty-library/src/handlers/download.rs index 4c26d9a..6b203c4 100644 --- a/rusty-library/src/handlers/download.rs +++ b/rusty-library/src/handlers/download.rs @@ -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(); diff --git a/rusty-library/src/handlers/error.rs b/rusty-library/src/handlers/error.rs index d3f89f1..176b431 100644 --- a/rusty-library/src/handlers/error.rs +++ b/rusty-library/src/handlers/error.rs @@ -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 { diff --git a/rusty-library/src/handlers/html/author.rs b/rusty-library/src/handlers/html/author.rs index 477bafe..d27c6d3 100644 --- a/rusty-library/src/handlers/html/author.rs +++ b/rusty-library/src/handlers/html/author.rs @@ -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) -> Result { let mut context = Context::new(); context.insert("title", &author.name); diff --git a/rusty-library/src/handlers/html/authors.rs b/rusty-library/src/handlers/html/authors.rs index e4d24a7..6db29d4 100644 --- a/rusty-library/src/handlers/html/authors.rs +++ b/rusty-library/src/handlers/html/authors.rs @@ -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>, diff --git a/rusty-library/src/handlers/html/books.rs b/rusty-library/src/handlers/html/books.rs index 0100204..14700ae 100644 --- a/rusty-library/src/handlers/html/books.rs +++ b/rusty-library/src/handlers/html/books.rs @@ -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), ) diff --git a/rusty-library/src/handlers/html/recent.rs b/rusty-library/src/handlers/html/recent.rs index 9f15e7b..05c142f 100644 --- a/rusty-library/src/handlers/html/recent.rs +++ b/rusty-library/src/handlers/html/recent.rs @@ -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) -> Result { let mut context = Context::new(); context.insert("title", "Recent Books"); diff --git a/rusty-library/src/handlers/html/series.rs b/rusty-library/src/handlers/html/series.rs index d0942bb..856939d 100644 --- a/rusty-library/src/handlers/html/series.rs +++ b/rusty-library/src/handlers/html/series.rs @@ -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>, diff --git a/rusty-library/src/handlers/html/series_single.rs b/rusty-library/src/handlers/html/series_single.rs index d9c1dc9..148aad4 100644 --- a/rusty-library/src/handlers/html/series_single.rs +++ b/rusty-library/src/handlers/html/series_single.rs @@ -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) -> Result { let mut context = Context::new(); context.insert("title", &series.name); diff --git a/rusty-library/src/handlers/opds/author.rs b/rusty-library/src/handlers/opds/author.rs index ccd489d..c79af76 100644 --- a/rusty-library/src/handlers/opds/author.rs +++ b/rusty-library/src/handlers/opds/author.rs @@ -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) -> Result { let entries: Vec = books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/authors.rs b/rusty-library/src/handlers/opds/authors.rs index bc60380..afdc7aa 100644 --- a/rusty-library/src/handlers/opds/authors.rs +++ b/rusty-library/src/handlers/opds/authors.rs @@ -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>, diff --git a/rusty-library/src/handlers/opds/books.rs b/rusty-library/src/handlers/opds/books.rs index 0c23a8a..0e7541f 100644 --- a/rusty-library/src/handlers/opds/books.rs +++ b/rusty-library/src/handlers/opds/books.rs @@ -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>, diff --git a/rusty-library/src/handlers/opds/feed.rs b/rusty-library/src/handlers/opds/feed.rs index 8534818..1f1146e 100644 --- a/rusty-library/src/handlers/opds/feed.rs +++ b/rusty-library/src/handlers/opds/feed.rs @@ -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, poem::Error> { let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/recent.rs b/rusty-library/src/handlers/opds/recent.rs index d3197fe..d2e0fa2 100644 --- a/rusty-library/src/handlers/opds/recent.rs +++ b/rusty-library/src/handlers/opds/recent.rs @@ -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) -> Result { let entries: Vec = recent_books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/opds/series.rs b/rusty-library/src/handlers/opds/series.rs index 04598f8..afe31a7 100644 --- a/rusty-library/src/handlers/opds/series.rs +++ b/rusty-library/src/handlers/opds/series.rs @@ -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>, diff --git a/rusty-library/src/handlers/opds/series_single.rs b/rusty-library/src/handlers/opds/series_single.rs index 370ff47..ad8e5cb 100644 --- a/rusty-library/src/handlers/opds/series_single.rs +++ b/rusty-library/src/handlers/opds/series_single.rs @@ -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) -> Result { let entries: Vec = books.into_iter().map(Entry::from).collect(); let now = OffsetDateTime::now_utc(); diff --git a/rusty-library/src/handlers/paginated.rs b/rusty-library/src/handlers/paginated.rs index c024032..76a92af 100644 --- a/rusty-library/src/handlers/paginated.rs +++ b/rusty-library/src/handlers/paginated.rs @@ -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( template: &str, fetcher: F, diff --git a/rusty-library/src/handlers/recent.rs b/rusty-library/src/handlers/recent.rs index ec630ef..44f268c 100644 --- a/rusty-library/src/handlers/recent.rs +++ b/rusty-library/src/handlers/recent.rs @@ -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>, diff --git a/rusty-library/src/handlers/series.rs b/rusty-library/src/handlers/series.rs index e48bcf5..9c701e9 100644 --- a/rusty-library/src/handlers/series.rs +++ b/rusty-library/src/handlers/series.rs @@ -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)>, diff --git a/rusty-library/src/handlers/series_single.rs b/rusty-library/src/handlers/series_single.rs index 1470e79..7b41b7a 100644 --- a/rusty-library/src/handlers/series_single.rs +++ b/rusty-library/src/handlers/series_single.rs @@ -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, diff --git a/rusty-library/src/lib.rs b/rusty-library/src/lib.rs index 9645fcc..f2228aa 100644 --- a/rusty-library/src/lib.rs +++ b/rusty-library/src/lib.rs @@ -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 }); diff --git a/rusty-library/src/opds/author.rs b/rusty-library/src/opds/author.rs index 6350245..72ba315 100644 --- a/rusty-library/src/opds/author.rs +++ b/rusty-library/src/opds/author.rs @@ -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, } diff --git a/rusty-library/src/opds/content.rs b/rusty-library/src/opds/content.rs index accc7ee..d9de97c 100644 --- a/rusty-library/src/opds/content.rs +++ b/rusty-library/src/opds/content.rs @@ -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, } diff --git a/rusty-library/src/opds/entry.rs b/rusty-library/src/opds/entry.rs index 39f5345..2c601dc 100644 --- a/rusty-library/src/opds/entry.rs +++ b/rusty-library/src/opds/entry.rs @@ -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, #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, + /// List of links, for example to download an entry. #[serde(rename = "link")] pub links: Vec, } +/// 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 for Entry { fn from(value: Book) -> Self { let author = Author { @@ -31,7 +44,7 @@ impl From 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 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 for Entry { } } +/// Convert author information into an OPDS entry. +/// +/// Add the author link. impl From for Entry { fn from(value: DbAuthor) -> Self { let links = vec![Link { @@ -81,6 +97,9 @@ impl From for Entry { } } +/// Convert series information into an OPDS entry. +/// +/// Add the series link. impl From for Entry { fn from(value: Series) -> Self { let links = vec![Link { diff --git a/rusty-library/src/opds/error.rs b/rusty-library/src/opds/error.rs index c644cab..2bc0806 100644 --- a/rusty-library/src/opds/error.rs +++ b/rusty-library/src/opds/error.rs @@ -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), } diff --git a/rusty-library/src/opds/feed.rs b/rusty-library/src/opds/feed.rs index 6d02ece..022f1cc 100644 --- a/rusty-library/src/opds/feed.rs +++ b/rusty-library/src/opds/feed.rs @@ -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, + /// Entries inside the feed (books, series, subsections, ...) #[serde(rename = "entry")] pub entries: Vec, } 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 { let xml = to_string(&self)?; let mut reader = Reader::from_str(&xml); diff --git a/rusty-library/src/opds/link.rs b/rusty-library/src/opds/link.rs index 1cfe1ba..f19e28f 100644 --- a/rusty-library/src/opds/link.rs +++ b/rusty-library/src/opds/link.rs @@ -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, + /// Optional count (how many entries at the target). #[serde(rename = "@thr:count")] #[serde(skip_serializing_if = "Option::is_none")] pub count: Option, } +/// 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()), diff --git a/rusty-library/src/opds/media_type.rs b/rusty-library/src/opds/media_type.rs index a9547ce..7a98869 100644 --- a/rusty-library/src/opds/media_type.rs +++ b/rusty-library/src/opds/media_type.rs @@ -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 for MediaType { fn from(value: Format) -> Self { match value.0.as_ref() { @@ -23,6 +29,7 @@ impl From 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 { diff --git a/rusty-library/src/opds/mod.rs b/rusty-library/src/opds/mod.rs deleted file mode 100644 index dcd52e0..0000000 --- a/rusty-library/src/opds/mod.rs +++ /dev/null @@ -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; diff --git a/rusty-library/src/opds/relation.rs b/rusty-library/src/opds/relation.rs index 52ac158..8a9a9d3 100644 --- a/rusty-library/src/opds/relation.rs +++ b/rusty-library/src/opds/relation.rs @@ -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 for Relation { fn from(value: MediaType) -> Self { match value { @@ -26,6 +33,7 @@ impl From 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 { diff --git a/rusty-library/src/templates.rs b/rusty-library/src/templates.rs index 532f84c..c0c7cd8 100644 --- a/rusty-library/src/templates.rs +++ b/rusty-library/src/templates.rs @@ -1,6 +1,9 @@ +//! Tera templates. + use once_cell::sync::Lazy; use tera::Tera; +/// All tera templates, globally accessible. pub static TEMPLATES: Lazy = Lazy::new(|| { let mut tera = Tera::default(); tera.add_raw_templates(vec![ diff --git a/rusty-library/templates/base.html b/rusty-library/templates/base.html index 2ade26a..d4964ef 100644 --- a/rusty-library/templates/base.html +++ b/rusty-library/templates/base.html @@ -27,7 +27,11 @@