bit of documentation
This commit is contained in:
parent
a41dcab889
commit
870f457f1b
@ -1,3 +1,5 @@
|
|||||||
|
//! Bundle all functions together.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use r2d2::Pool;
|
use r2d2::Pool;
|
||||||
@ -7,12 +9,17 @@ use crate::data::{
|
|||||||
author::Author, book::Book, error::DataStoreError, pagination::SortOrder, series::Series,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Calibre {
|
pub struct Calibre {
|
||||||
pool: Pool<SqliteConnectionManager>,
|
pool: Pool<SqliteConnectionManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Calibre {
|
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<Self, DataStoreError> {
|
pub fn load(path: &Path) -> Result<Self, DataStoreError> {
|
||||||
let manager = SqliteConnectionManager::file(path);
|
let manager = SqliteConnectionManager::file(path);
|
||||||
let pool = r2d2::Pool::new(manager)?;
|
let pool = r2d2::Pool::new(manager)?;
|
||||||
@ -20,6 +27,8 @@ impl Calibre {
|
|||||||
Ok(Self { pool })
|
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(
|
pub fn books(
|
||||||
&self,
|
&self,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
@ -30,6 +39,8 @@ impl Calibre {
|
|||||||
Book::multiple(&conn, limit, cursor, sort_order)
|
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(
|
pub fn authors(
|
||||||
&self,
|
&self,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
@ -40,6 +51,8 @@ impl Calibre {
|
|||||||
Author::multiple(&conn, limit, cursor, sort_order)
|
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(
|
pub fn author_books(
|
||||||
&self,
|
&self,
|
||||||
author_id: u64,
|
author_id: u64,
|
||||||
@ -51,21 +64,26 @@ impl Calibre {
|
|||||||
Book::author_books(&conn, author_id, limit, cursor, sort_order)
|
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<Vec<Book>, DataStoreError> {
|
pub fn recent_books(&self, limit: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Book::recents(&conn, limit)
|
Book::recents(&conn, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a single book, specified `id`.
|
||||||
pub fn scalar_book(&self, id: u64) -> Result<Book, DataStoreError> {
|
pub fn scalar_book(&self, id: u64) -> Result<Book, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Book::scalar_book(&conn, id)
|
Book::scalar_book(&conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the author to a book with id `id`.
|
||||||
pub fn book_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
pub fn book_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::book_author(&conn, id)
|
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(
|
pub fn series(
|
||||||
&self,
|
&self,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
@ -76,51 +94,61 @@ impl Calibre {
|
|||||||
Series::multiple(&conn, limit, cursor, sort_order)
|
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<Option<(Series, f64)>, DataStoreError> {
|
pub fn book_series(&self, id: u64) -> Result<Option<(Series, f64)>, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Series::book_series(&conn, id)
|
Series::book_series(&conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all books belonging to the series with id `id`.
|
||||||
pub fn series_books(&self, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
pub fn series_books(&self, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Book::series_books(&conn, id)
|
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<bool, DataStoreError> {
|
pub fn has_previous_authors(&self, author_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::has_previous_authors(&conn, author_sort)
|
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<bool, DataStoreError> {
|
pub fn has_more_authors(&self, author_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::has_more_authors(&conn, author_sort)
|
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<bool, DataStoreError> {
|
pub fn has_previous_books(&self, book_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Book::has_previous_books(&conn, book_sort)
|
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<bool, DataStoreError> {
|
pub fn has_more_books(&self, book_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Book::has_more_books(&conn, book_sort)
|
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<bool, DataStoreError> {
|
pub fn has_previous_series(&self, series_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Series::has_previous_series(&conn, series_sort)
|
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<bool, DataStoreError> {
|
pub fn has_more_series(&self, series_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Series::has_more_series(&conn, series_sort)
|
Series::has_more_series(&conn, series_sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch a single author with id `id`.
|
||||||
pub fn scalar_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
pub fn scalar_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::scalar_author(&conn, id)
|
Author::scalar_author(&conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch a single series with id `id`.
|
||||||
pub fn scalar_series(&self, id: u64) -> Result<Series, DataStoreError> {
|
pub fn scalar_series(&self, id: u64) -> Result<Series, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Series::scalar_series(&conn, id)
|
Series::scalar_series(&conn, id)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Author data.
|
||||||
|
|
||||||
use rusqlite::{named_params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -6,10 +8,14 @@ use super::{
|
|||||||
pagination::{Pagination, SortOrder},
|
pagination::{Pagination, SortOrder},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Author in calibre.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
|
/// Id in database.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
/// Full name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Full name for sorting.
|
||||||
pub sort: String,
|
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(
|
pub fn multiple(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
limit: u64,
|
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<Self, DataStoreError> {
|
pub fn book_author(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT authors.id, authors.name, authors.sort FROM authors \
|
"SELECT authors.id, authors.name, authors.sort FROM authors \
|
||||||
@ -47,12 +56,14 @@ impl Author {
|
|||||||
Ok(stmt.query_row(params, Self::from_row)?)
|
Ok(stmt.query_row(params, Self::from_row)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch a single author with id `id`.
|
||||||
pub fn scalar_author(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
pub fn scalar_author(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
let mut stmt = conn.prepare("SELECT id, name, sort FROM authors WHERE id = (:id)")?;
|
let mut stmt = conn.prepare("SELECT id, name, sort FROM authors WHERE id = (:id)")?;
|
||||||
let params = named_params! { ":id": id };
|
let params = named_params! { ":id": id };
|
||||||
Ok(stmt.query_row(params, Self::from_row)?)
|
Ok(stmt.query_row(params, Self::from_row)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if there are more authors before the specified cursor.
|
||||||
pub fn has_previous_authors(
|
pub fn has_previous_authors(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
sort_name: &str,
|
sort_name: &str,
|
||||||
@ -60,6 +71,7 @@ impl Author {
|
|||||||
Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::DESC)
|
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<bool, DataStoreError> {
|
pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::ASC)
|
Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::ASC)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Book data.
|
||||||
|
|
||||||
use rusqlite::{named_params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -7,14 +9,22 @@ use super::{
|
|||||||
pagination::{Pagination, SortOrder},
|
pagination::{Pagination, SortOrder},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
/// Book in calibre.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
|
/// Id in database.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
/// Book title.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
/// Book title for sorting.
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
|
/// Folder of the book within the calibre library.
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
/// Uuid of the book.
|
||||||
pub uuid: String,
|
pub uuid: String,
|
||||||
|
/// When was the book last modified.
|
||||||
pub last_modified: OffsetDateTime,
|
pub last_modified: OffsetDateTime,
|
||||||
|
/// Optional description.
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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(
|
pub fn multiple(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
limit: u64,
|
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(
|
pub fn author_books(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
author_id: u64,
|
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<Vec<Book>, DataStoreError> {
|
pub fn series_books(conn: &Connection, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text FROM series \
|
"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())
|
Ok(iter.filter_map(Result::ok).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get recent books up to a limit of `limit`.
|
||||||
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \
|
"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())
|
Ok(iter.filter_map(Result::ok).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a single book, specified `id`.
|
||||||
pub fn scalar_book(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
pub fn scalar_book(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT books.id, books.title, books.sort, books.path, books.uuid, books.last_modified, comments.text \
|
"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)?)
|
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<bool, DataStoreError> {
|
pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
||||||
Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::DESC)
|
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<bool, DataStoreError> {
|
pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
||||||
Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC)
|
Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC)
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,28 @@
|
|||||||
|
//! Error handling for calibre database access.
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use time::error::Parse;
|
use time::error::Parse;
|
||||||
|
|
||||||
|
/// Errors from accessing the calibre database.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("data store error")]
|
#[error("data store error")]
|
||||||
pub enum DataStoreError {
|
pub enum DataStoreError {
|
||||||
|
/// Found no entries for the query.
|
||||||
#[error("no results")]
|
#[error("no results")]
|
||||||
NoResults(rusqlite::Error),
|
NoResults(rusqlite::Error),
|
||||||
|
/// Error with SQLite.
|
||||||
#[error("sqlite error")]
|
#[error("sqlite error")]
|
||||||
SqliteError(rusqlite::Error),
|
SqliteError(rusqlite::Error),
|
||||||
|
/// Error connecting to the database.
|
||||||
#[error("connection error")]
|
#[error("connection error")]
|
||||||
ConnectionError(#[from] r2d2::Error),
|
ConnectionError(#[from] r2d2::Error),
|
||||||
|
/// Error wparsing a datetime from the database.
|
||||||
#[error("failed to parse datetime")]
|
#[error("failed to parse datetime")]
|
||||||
DateTimeError(#[from] Parse),
|
DateTimeError(#[from] Parse),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert an SQLite error into a proper NoResults one if the query
|
||||||
|
/// returned no rows, return others as is.
|
||||||
impl From<rusqlite::Error> for DataStoreError {
|
impl From<rusqlite::Error> for DataStoreError {
|
||||||
fn from(error: rusqlite::Error) -> Self {
|
fn from(error: rusqlite::Error) -> Self {
|
||||||
match error {
|
match error {
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
|
//! Cursor pagination handling.
|
||||||
|
|
||||||
use rusqlite::{named_params, Connection, Row, ToSql};
|
use rusqlite::{named_params, Connection, Row, ToSql};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::error::DataStoreError;
|
use super::error::DataStoreError;
|
||||||
|
|
||||||
|
/// How to sort query results. Signifying whether we are paginating forwards or backwards.
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
pub enum SortOrder {
|
pub enum SortOrder {
|
||||||
|
/// Forwards
|
||||||
ASC,
|
ASC,
|
||||||
|
/// Backwards
|
||||||
DESC,
|
DESC,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pagination data.
|
||||||
pub struct Pagination<'a> {
|
pub struct Pagination<'a> {
|
||||||
|
/// Sort by this column.
|
||||||
pub sort_col: &'a str,
|
pub sort_col: &'a str,
|
||||||
|
/// Limit returned results.
|
||||||
pub limit: u64,
|
pub limit: u64,
|
||||||
|
/// Where to start paginating.
|
||||||
pub cursor: Option<&'a str>,
|
pub cursor: Option<&'a str>,
|
||||||
|
/// Paginating forwards or backwards.
|
||||||
pub sort_order: SortOrder,
|
pub sort_order: SortOrder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Pagination<'a> {
|
impl<'a> Pagination<'a> {
|
||||||
|
/// Create a new pagination.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
sort_col: &'a str,
|
sort_col: &'a str,
|
||||||
cursor: Option<&'a str>,
|
cursor: Option<&'a str>,
|
||||||
@ -40,6 +51,7 @@ impl<'a> Pagination<'a> {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if there are more items forwards or backwards from `cursor` (direction specified by `sort_order`).
|
||||||
pub fn has_prev_or_more(
|
pub fn has_prev_or_more(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
table: &str,
|
table: &str,
|
||||||
@ -57,6 +69,7 @@ impl<'a> Pagination<'a> {
|
|||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paginate a statement.
|
||||||
pub fn paginate<T, F>(
|
pub fn paginate<T, F>(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
@ -77,7 +90,8 @@ impl<'a> Pagination<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let sort_col = self.sort_col;
|
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 sort_col_wrapped = if let Some(index) = sort_col.find('.') {
|
||||||
let right_part = &sort_col[index..];
|
let right_part = &sort_col[index..];
|
||||||
"t".to_owned() + right_part
|
"t".to_owned() + right_part
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Series data.
|
||||||
|
|
||||||
use rusqlite::{named_params, Connection, Row};
|
use rusqlite::{named_params, Connection, Row};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
@ -6,10 +8,14 @@ use super::{
|
|||||||
pagination::{Pagination, SortOrder},
|
pagination::{Pagination, SortOrder},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Series in calibre.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Series {
|
pub struct Series {
|
||||||
|
/// Id in database.
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
/// Series name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Series name for sorting.
|
||||||
pub sort: String,
|
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(
|
pub fn multiple(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
@ -37,12 +45,14 @@ impl Series {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch a single series with id `id`.
|
||||||
pub fn scalar_series(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
pub fn scalar_series(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
let mut stmt = conn.prepare("SELECT id, name, sort FROM series WHERE id = (:id)")?;
|
let mut stmt = conn.prepare("SELECT id, name, sort FROM series WHERE id = (:id)")?;
|
||||||
let params = named_params! { ":id": id };
|
let params = named_params! { ":id": id };
|
||||||
Ok(stmt.query_row(params, Self::from_row)?)
|
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(
|
pub fn book_series(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
book_id: u64,
|
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<bool, DataStoreError> {
|
pub fn has_previous_series(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::DESC)
|
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<bool, DataStoreError> {
|
pub fn has_more_series(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::ASC)
|
Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::ASC)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
//! Read data from a calibre library, leveraging its SQLite metadata database.
|
||||||
|
|
||||||
pub mod calibre;
|
pub mod calibre;
|
||||||
|
/// Data structs for the calibre database.
|
||||||
pub mod data {
|
pub mod data {
|
||||||
pub mod author;
|
pub mod author;
|
||||||
pub mod book;
|
pub mod book;
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
//! Data for global app state.
|
||||||
|
|
||||||
use calibre_db::calibre::Calibre;
|
use calibre_db::calibre::Calibre;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
|
/// Global application state, meant to be used in request handlers.
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
/// Access calibre database.
|
||||||
pub calibre: Calibre,
|
pub calibre: Calibre,
|
||||||
|
/// Access application configuration.
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Cli interface.
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
/// Simple opds server for calibre
|
/// Simple opds server for calibre
|
||||||
|
@ -1,23 +1,32 @@
|
|||||||
|
//! Configuration data.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
|
|
||||||
|
/// Errors when dealing with application configuration.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ConfigError {
|
pub enum ConfigError {
|
||||||
|
/// Calibre library path does not exist.
|
||||||
#[error("no folder at {0}")]
|
#[error("no folder at {0}")]
|
||||||
LibraryPathNotFound(String),
|
LibraryPathNotFound(String),
|
||||||
|
/// Calibre database does not exist.
|
||||||
#[error("no metadata.db in {0}")]
|
#[error("no metadata.db in {0}")]
|
||||||
MetadataNotFound(String),
|
MetadataNotFound(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Application configuration.
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Calibre library folder.
|
||||||
pub library_path: PathBuf,
|
pub library_path: PathBuf,
|
||||||
|
/// Calibre metadata file path.
|
||||||
pub metadata_path: PathBuf,
|
pub metadata_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
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> {
|
pub fn load(args: &Cli) -> Result<Self, ConfigError> {
|
||||||
let library_path = Path::new(&args.library_path).to_path_buf();
|
let library_path = Path::new(&args.library_path).to_path_buf();
|
||||||
|
|
||||||
|
@ -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 std::{collections::HashMap, fmt::Display, path::Path};
|
||||||
|
|
||||||
use calibre_db::data::{
|
use calibre_db::data::{
|
||||||
author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries,
|
author::Author as DbAuthor, book::Book as DbBook, series::Series as DbSeries,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
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)]
|
#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash)]
|
||||||
pub struct Format(pub String);
|
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>;
|
pub type Formats = HashMap<Format, String>;
|
||||||
|
|
||||||
|
/// Recognize `pdf` and `epub` and return their value, everything else transforms to `unknown`.
|
||||||
impl Display for Format {
|
impl Display for Format {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self.0.as_ref() {
|
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)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Book {
|
pub struct Book {
|
||||||
pub id: u64,
|
/// Book data from the database.
|
||||||
pub title: String,
|
pub data: DbBook,
|
||||||
pub sort: String,
|
/// Author information.
|
||||||
pub path: String,
|
|
||||||
pub uuid: String,
|
|
||||||
pub last_modified: OffsetDateTime,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub author: DbAuthor,
|
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)>,
|
pub series: Option<(DbSeries, f64)>,
|
||||||
|
/// Format information.
|
||||||
pub formats: Formats,
|
pub formats: Formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Book {
|
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(
|
pub fn from_db_book(
|
||||||
db_book: &DbBook,
|
db_book: &DbBook,
|
||||||
db_series: Option<(DbSeries, f64)>,
|
db_series: Option<(DbSeries, f64)>,
|
||||||
@ -44,19 +58,15 @@ impl Book {
|
|||||||
formats: Formats,
|
formats: Formats,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: db_book.id,
|
data: db_book.clone(),
|
||||||
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,
|
|
||||||
author: author.clone(),
|
author: author.clone(),
|
||||||
series: db_series.map(|x| (x.0, x.1)),
|
series: db_series.map(|x| (x.0, x.1)),
|
||||||
formats,
|
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 {
|
fn formats(book: &DbBook, library_path: &Path) -> Formats {
|
||||||
let book_path = library_path.join(&book.path);
|
let book_path = library_path.join(&book.path);
|
||||||
let mut formats = HashMap::new();
|
let mut formats = HashMap::new();
|
||||||
@ -80,6 +90,8 @@ impl Book {
|
|||||||
formats
|
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> {
|
pub fn full_book(book: &DbBook, state: &AppState) -> Option<Book> {
|
||||||
let formats = Book::formats(book, &state.config.library_path);
|
let formats = Book::formats(book, &state.config.library_path);
|
||||||
let author = state.calibre.book_author(book.id).ok()?;
|
let author = state.calibre.book_author(book.id).ok()?;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for a single author.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use calibre_db::data::pagination::SortOrder;
|
use calibre_db::data::pagination::SortOrder;
|
||||||
@ -9,6 +11,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for multiple authors.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
||||||
@ -9,6 +11,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, Accept};
|
use crate::{app_state::AppState, Accept};
|
||||||
|
|
||||||
|
/// Handle a request for multiple authors, starting at the first.
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler_init(
|
pub async fn handler_init(
|
||||||
accept: Data<&Accept>,
|
accept: Data<&Accept>,
|
||||||
@ -17,6 +20,8 @@ pub async fn handler_init(
|
|||||||
authors(&accept, &state.calibre, None, &SortOrder::ASC).await
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for multiple books.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use calibre_db::data::pagination::SortOrder;
|
use calibre_db::data::pagination::SortOrder;
|
||||||
@ -9,6 +11,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, Accept};
|
use crate::{app_state::AppState, Accept};
|
||||||
|
|
||||||
|
/// Handle a request for multiple books, starting at the first.
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler_init(
|
pub async fn handler_init(
|
||||||
accept: Data<&Accept>,
|
accept: Data<&Accept>,
|
||||||
@ -17,6 +20,8 @@ pub async fn handler_init(
|
|||||||
books(&accept, &state, None, &SortOrder::ASC).await
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for cover images.
|
||||||
|
|
||||||
use std::{fs::File, io::Read, sync::Arc};
|
use std::{fs::File, io::Read, sync::Arc};
|
||||||
|
|
||||||
use poem::{
|
use poem::{
|
||||||
@ -9,6 +11,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, handlers::error::HandlerError};
|
use crate::{app_state::AppState, handlers::error::HandlerError};
|
||||||
|
|
||||||
|
/// Handle a request for the cover image of book with id `id`.
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for specific formats of a book.
|
||||||
|
|
||||||
use std::{fs::File, io::Read, sync::Arc};
|
use std::{fs::File, io::Read, sync::Arc};
|
||||||
|
|
||||||
use poem::{
|
use poem::{
|
||||||
@ -13,6 +15,7 @@ use crate::{
|
|||||||
handlers::error::HandlerError,
|
handlers::error::HandlerError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Handle a request for a book with id `id` in format `format`.
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
Path((id, format)): Path<(u64, String)>,
|
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 book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
|
||||||
let format = Format(format);
|
let format = Format(format);
|
||||||
let file_name = book.formats.get(&format).ok_or(NotFoundError)?;
|
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 file = File::open(file_path).map_err(|_| NotFoundError)?;
|
||||||
|
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Error handling for requests handlers.
|
||||||
|
|
||||||
use calibre_db::data::error::DataStoreError;
|
use calibre_db::data::error::DataStoreError;
|
||||||
use poem::{error::ResponseError, http::StatusCode, Body, Response};
|
use poem::{error::ResponseError, http::StatusCode, Body, Response};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -6,15 +8,22 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::opds::error::OpdsError;
|
use crate::opds::error::OpdsError;
|
||||||
|
|
||||||
|
/// Errors happening during handling of requests.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("opds error")]
|
#[error("opds error")]
|
||||||
pub enum HandlerError {
|
pub enum HandlerError {
|
||||||
|
/// Error rendering OPDS.
|
||||||
#[error("opds error")]
|
#[error("opds error")]
|
||||||
OpdsError(#[from] OpdsError),
|
OpdsError(#[from] OpdsError),
|
||||||
|
/// Error fetching data from calibre.
|
||||||
#[error("data error")]
|
#[error("data error")]
|
||||||
DataError(#[from] DataStoreError),
|
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 {
|
impl ResponseError for HandlerError {
|
||||||
fn status(&self) -> StatusCode {
|
fn status(&self) -> StatusCode {
|
||||||
match &self {
|
match &self {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
//! Handle a single author for html.
|
||||||
|
|
||||||
use calibre_db::data::author::Author;
|
use calibre_db::data::author::Author;
|
||||||
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::{data::book::Book, templates::TEMPLATES};
|
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> {
|
pub async fn handler(author: Author, books: Vec<Book>) -> Result<Response, poem::Error> {
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("title", &author.name);
|
context.insert("title", &author.name);
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
//! Handle multiple authors in html.
|
||||||
|
|
||||||
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
||||||
use poem::Response;
|
use poem::Response;
|
||||||
|
|
||||||
use crate::handlers::paginated;
|
use crate::handlers::paginated;
|
||||||
|
|
||||||
|
/// Render all authors paginated by cursor in html.
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
calibre: &Calibre,
|
calibre: &Calibre,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
//! Handle multiple books in html.
|
||||||
|
|
||||||
use calibre_db::data::pagination::SortOrder;
|
use calibre_db::data::pagination::SortOrder;
|
||||||
use poem::Response;
|
use poem::Response;
|
||||||
|
|
||||||
use crate::{app_state::AppState, data::book::Book, handlers::paginated};
|
use crate::{app_state::AppState, data::book::Book, handlers::paginated};
|
||||||
|
|
||||||
|
/// Render all books paginated by cursor in html.
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
@ -16,7 +19,7 @@ pub async fn handler(
|
|||||||
.books(25, cursor, sort_order)
|
.books(25, cursor, sort_order)
|
||||||
.map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect())
|
.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_previous_books(cursor),
|
||||||
|cursor| state.calibre.has_more_books(cursor),
|
|cursor| state.calibre.has_more_books(cursor),
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
//! Handle recent books in html.
|
||||||
|
|
||||||
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::{data::book::Book, templates::TEMPLATES};
|
use crate::{data::book::Book, templates::TEMPLATES};
|
||||||
|
|
||||||
|
/// Render recent books as html.
|
||||||
pub async fn handler(recent_books: Vec<Book>) -> Result<Response, poem::Error> {
|
pub async fn handler(recent_books: Vec<Book>) -> Result<Response, poem::Error> {
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("title", "Recent Books");
|
context.insert("title", "Recent Books");
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
//! Handle multiple series in html.
|
||||||
|
|
||||||
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
||||||
use poem::Response;
|
use poem::Response;
|
||||||
|
|
||||||
use crate::handlers::paginated;
|
use crate::handlers::paginated;
|
||||||
|
|
||||||
|
/// Render all series paginated by cursor as html.
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
calibre: &Calibre,
|
calibre: &Calibre,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
//! Handle a single series in html.
|
||||||
|
|
||||||
use calibre_db::data::series::Series;
|
use calibre_db::data::series::Series;
|
||||||
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
use poem::{error::InternalServerError, web::Html, IntoResponse, Response};
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::{data::book::Book, templates::TEMPLATES};
|
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> {
|
pub async fn handler(series: Series, books: Vec<Book>) -> Result<Response, poem::Error> {
|
||||||
let mut context = Context::new();
|
let mut context = Context::new();
|
||||||
context.insert("title", &series.name);
|
context.insert("title", &series.name);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle a single author for opds.
|
||||||
|
|
||||||
use calibre_db::data::author::Author;
|
use calibre_db::data::author::Author;
|
||||||
use poem::{IntoResponse, Response};
|
use poem::{IntoResponse, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -8,6 +10,7 @@ use crate::{
|
|||||||
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
|
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> {
|
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 entries: Vec<Entry> = books.into_iter().map(Entry::from).collect();
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle multiple authors for opds.
|
||||||
|
|
||||||
use calibre_db::{
|
use calibre_db::{
|
||||||
calibre::Calibre,
|
calibre::Calibre,
|
||||||
data::{author::Author as DbAuthor, pagination::SortOrder},
|
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},
|
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(
|
pub async fn handler(
|
||||||
calibre: &Calibre,
|
calibre: &Calibre,
|
||||||
_cursor: Option<&str>,
|
_cursor: Option<&str>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle multiple books for opds.
|
||||||
|
|
||||||
use calibre_db::data::pagination::SortOrder;
|
use calibre_db::data::pagination::SortOrder;
|
||||||
use poem::{IntoResponse, Response};
|
use poem::{IntoResponse, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -9,6 +11,7 @@ use crate::{
|
|||||||
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
|
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(
|
pub async fn handler(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
_cursor: Option<&str>,
|
_cursor: Option<&str>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle the OPDS root feed.
|
||||||
|
|
||||||
use poem::{handler, web::WithContentType, IntoResponse};
|
use poem::{handler, web::WithContentType, IntoResponse};
|
||||||
use time::OffsetDateTime;
|
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]
|
#[handler]
|
||||||
pub async fn handler() -> Result<WithContentType<String>, poem::Error> {
|
pub async fn handler() -> Result<WithContentType<String>, poem::Error> {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle recent books for OPDS.
|
||||||
|
|
||||||
use poem::{IntoResponse, Response};
|
use poem::{IntoResponse, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
@ -7,6 +9,7 @@ use crate::{
|
|||||||
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
|
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> {
|
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 entries: Vec<Entry> = recent_books.into_iter().map(Entry::from).collect();
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle multiple series for OPDS.
|
||||||
|
|
||||||
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
use calibre_db::{calibre::Calibre, data::pagination::SortOrder};
|
||||||
use poem::{IntoResponse, Response};
|
use poem::{IntoResponse, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -7,6 +9,7 @@ use crate::{
|
|||||||
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
|
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(
|
pub async fn handler(
|
||||||
calibre: &Calibre,
|
calibre: &Calibre,
|
||||||
_cursor: Option<&str>,
|
_cursor: Option<&str>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle a single series for opds.
|
||||||
|
|
||||||
use calibre_db::data::series::Series;
|
use calibre_db::data::series::Series;
|
||||||
use poem::{IntoResponse, Response};
|
use poem::{IntoResponse, Response};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -8,6 +10,7 @@ use crate::{
|
|||||||
opds::{entry::Entry, feed::Feed, link::Link, media_type::MediaType, relation::Relation},
|
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> {
|
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 entries: Vec<Entry> = books.into_iter().map(Entry::from).collect();
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Deal with cursor pagination.
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use calibre_db::data::error::DataStoreError;
|
use calibre_db::data::error::DataStoreError;
|
||||||
@ -9,6 +11,7 @@ use crate::templates::TEMPLATES;
|
|||||||
|
|
||||||
use super::error::HandlerError;
|
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>(
|
pub fn render<T: Serialize + Debug, F, S, P, M>(
|
||||||
template: &str,
|
template: &str,
|
||||||
fetcher: F,
|
fetcher: F,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
//! Handle requests for recent books.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use poem::{handler, web::Data, Response};
|
use poem::{handler, web::Data, Response};
|
||||||
|
|
||||||
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
accept: Data<&Accept>,
|
accept: Data<&Accept>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for multiple series.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use calibre_db::data::pagination::SortOrder;
|
use calibre_db::data::pagination::SortOrder;
|
||||||
@ -9,6 +11,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, Accept};
|
use crate::{app_state::AppState, Accept};
|
||||||
|
|
||||||
|
/// Handle a request for multiple series, starting at the first.
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler_init(
|
pub async fn handler_init(
|
||||||
accept: Data<&Accept>,
|
accept: Data<&Accept>,
|
||||||
@ -17,6 +20,8 @@ pub async fn handler_init(
|
|||||||
series(&accept, &state, None, &SortOrder::ASC).await
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Handle requests for a single series.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use poem::{
|
use poem::{
|
||||||
@ -8,6 +10,7 @@ use poem::{
|
|||||||
|
|
||||||
use crate::{app_state::AppState, data::book::Book, handlers::error::HandlerError, Accept};
|
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]
|
#[handler]
|
||||||
pub async fn handler(
|
pub async fn handler(
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
|
@ -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 std::sync::Arc;
|
||||||
|
|
||||||
use app_state::AppState;
|
use app_state::AppState;
|
||||||
@ -10,13 +14,17 @@ use poem::{
|
|||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
pub mod app_state;
|
pub mod app_state;
|
||||||
pub mod basic_auth;
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
/// Data structs and their functions.
|
||||||
pub mod data {
|
pub mod data {
|
||||||
pub mod book;
|
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 {
|
pub mod handlers {
|
||||||
|
/// Handle requests for html.
|
||||||
pub mod html {
|
pub mod html {
|
||||||
pub mod author;
|
pub mod author;
|
||||||
pub mod authors;
|
pub mod authors;
|
||||||
@ -25,6 +33,7 @@ pub mod handlers {
|
|||||||
pub mod series;
|
pub mod series;
|
||||||
pub mod series_single;
|
pub mod series_single;
|
||||||
}
|
}
|
||||||
|
/// Handle requests for OPDS.
|
||||||
pub mod opds {
|
pub mod opds {
|
||||||
pub mod author;
|
pub mod author;
|
||||||
pub mod authors;
|
pub mod authors;
|
||||||
@ -45,19 +54,34 @@ pub mod handlers {
|
|||||||
pub mod series;
|
pub mod series;
|
||||||
pub mod series_single;
|
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;
|
pub mod templates;
|
||||||
|
|
||||||
|
/// Internal marker data in lieu of a proper `Accept` header.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Accept {
|
pub enum Accept {
|
||||||
|
/// Render as html.
|
||||||
Html,
|
Html,
|
||||||
|
/// Render as OPDS.
|
||||||
Opds,
|
Opds,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Embedd static files.
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "static"]
|
#[folder = "static"]
|
||||||
pub struct Files;
|
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> {
|
pub async fn run(config: Config) -> Result<(), std::io::Error> {
|
||||||
let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database");
|
let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database");
|
||||||
let app_state = Arc::new(AppState { calibre, config });
|
let app_state = Arc::new(AppState { calibre, config });
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
|
//! Author data.
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Author information.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "author")]
|
#[serde(rename = "author")]
|
||||||
pub struct Author {
|
pub struct Author {
|
||||||
|
/// Full name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// Where to find the author.
|
||||||
pub uri: String,
|
pub uri: String,
|
||||||
|
/// Optional email address.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
|
//! Content data.
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::media_type::MediaType;
|
use super::media_type::MediaType;
|
||||||
|
|
||||||
|
/// Content of different types, used for example for description of an entry.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "content")]
|
#[serde(rename = "content")]
|
||||||
pub struct Content {
|
pub struct Content {
|
||||||
|
/// Media type of this content.
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
pub media_type: MediaType,
|
pub media_type: MediaType,
|
||||||
|
/// Actual content.
|
||||||
#[serde(rename = "$value")]
|
#[serde(rename = "$value")]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Entry data.
|
||||||
|
|
||||||
use calibre_db::data::{author::Author as DbAuthor, series::Series};
|
use calibre_db::data::{author::Author as DbAuthor, series::Series};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
@ -8,21 +10,32 @@ use super::{
|
|||||||
author::Author, content::Content, link::Link, media_type::MediaType, relation::Relation,
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "entry")]
|
#[serde(rename = "entry")]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
|
/// Title of the entry.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
/// Id, for example a uuid.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
/// When was this entry updated last.
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
pub updated: OffsetDateTime,
|
pub updated: OffsetDateTime,
|
||||||
|
/// Optional content.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
/// Optional author information.
|
||||||
pub content: Option<Content>,
|
pub content: Option<Content>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub author: Option<Author>,
|
pub author: Option<Author>,
|
||||||
|
/// List of links, for example to download an entry.
|
||||||
#[serde(rename = "link")]
|
#[serde(rename = "link")]
|
||||||
pub links: Vec<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 {
|
impl From<Book> for Entry {
|
||||||
fn from(value: Book) -> Self {
|
fn from(value: Book) -> Self {
|
||||||
let author = Author {
|
let author = Author {
|
||||||
@ -31,7 +44,7 @@ impl From<Book> for Entry {
|
|||||||
email: None,
|
email: None,
|
||||||
};
|
};
|
||||||
let mut links = vec![Link {
|
let mut links = vec![Link {
|
||||||
href: format!("/cover/{}", value.id),
|
href: format!("/cover/{}", value.data.id),
|
||||||
media_type: MediaType::Jpeg,
|
media_type: MediaType::Jpeg,
|
||||||
rel: Relation::Image,
|
rel: Relation::Image,
|
||||||
title: None,
|
title: None,
|
||||||
@ -44,15 +57,15 @@ impl From<Book> for Entry {
|
|||||||
.collect();
|
.collect();
|
||||||
links.append(&mut format_links);
|
links.append(&mut format_links);
|
||||||
|
|
||||||
let content = value.description.map(|desc| Content {
|
let content = value.data.description.map(|desc| Content {
|
||||||
media_type: MediaType::Html,
|
media_type: MediaType::Html,
|
||||||
content: desc,
|
content: desc,
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
title: value.title.clone(),
|
title: value.data.title.clone(),
|
||||||
id: format!("urn:uuid:{}", value.uuid),
|
id: format!("urn:uuid:{}", value.data.uuid),
|
||||||
updated: value.last_modified,
|
updated: value.data.last_modified,
|
||||||
content,
|
content,
|
||||||
author: Some(author),
|
author: Some(author),
|
||||||
links,
|
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 {
|
impl From<DbAuthor> for Entry {
|
||||||
fn from(value: DbAuthor) -> Self {
|
fn from(value: DbAuthor) -> Self {
|
||||||
let links = vec![Link {
|
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 {
|
impl From<Series> for Entry {
|
||||||
fn from(value: Series) -> Self {
|
fn from(value: Series) -> Self {
|
||||||
let links = vec![Link {
|
let links = vec![Link {
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
|
//! Error handling for OPDS data.
|
||||||
|
|
||||||
use std::string::FromUtf8Error;
|
use std::string::FromUtf8Error;
|
||||||
|
|
||||||
use quick_xml::DeError;
|
use quick_xml::DeError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errors happening during handling OPDS data.
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
#[error("opds error")]
|
#[error("opds error")]
|
||||||
pub enum OpdsError {
|
pub enum OpdsError {
|
||||||
|
/// Error serializing OPDS data.
|
||||||
#[error("failed to serialize struct")]
|
#[error("failed to serialize struct")]
|
||||||
SerializingError(#[from] DeError),
|
SerializingError(#[from] DeError),
|
||||||
|
/// Error parsing OPDS xml structure.
|
||||||
#[error("xml failure")]
|
#[error("xml failure")]
|
||||||
XmlError(#[from] quick_xml::Error),
|
XmlError(#[from] quick_xml::Error),
|
||||||
|
/// Error decoding xml as UTF-8.
|
||||||
#[error("failed to decode as utf-8")]
|
#[error("failed to decode as utf-8")]
|
||||||
Utf8Error(#[from] FromUtf8Error),
|
Utf8Error(#[from] FromUtf8Error),
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
//! Root feed data.
|
||||||
|
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
use quick_xml::{
|
use quick_xml::{
|
||||||
@ -13,22 +15,31 @@ use super::{
|
|||||||
relation::Relation,
|
relation::Relation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Root feed element of OPDS.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "feed")]
|
#[serde(rename = "feed")]
|
||||||
pub struct Feed {
|
pub struct Feed {
|
||||||
|
/// Title, often shown in OPDS clients.
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
/// Feed id.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
/// When was the feed updated last.
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
pub updated: OffsetDateTime,
|
pub updated: OffsetDateTime,
|
||||||
|
/// Icon for the feed.
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
|
/// Feed author.
|
||||||
pub author: Author,
|
pub author: Author,
|
||||||
|
/// Links, for example home or self.
|
||||||
#[serde(rename = "link")]
|
#[serde(rename = "link")]
|
||||||
pub links: Vec<Link>,
|
pub links: Vec<Link>,
|
||||||
|
/// Entries inside the feed (books, series, subsections, ...)
|
||||||
#[serde(rename = "entry")]
|
#[serde(rename = "entry")]
|
||||||
pub entries: Vec<Entry>,
|
pub entries: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Feed {
|
impl Feed {
|
||||||
|
/// Create a feed with the specified data.
|
||||||
pub fn create(
|
pub fn create(
|
||||||
now: OffsetDateTime,
|
now: OffsetDateTime,
|
||||||
id: &str,
|
id: &str,
|
||||||
@ -65,6 +76,7 @@ impl Feed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize a feed to OPDS xml.
|
||||||
pub fn as_xml(&self) -> Result<String, OpdsError> {
|
pub fn as_xml(&self) -> Result<String, OpdsError> {
|
||||||
let xml = to_string(&self)?;
|
let xml = to_string(&self)?;
|
||||||
let mut reader = Reader::from_str(&xml);
|
let mut reader = Reader::from_str(&xml);
|
||||||
|
@ -1,32 +1,41 @@
|
|||||||
|
//! Link data.
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::data::book::{Book, Format};
|
use crate::data::book::{Book, Format};
|
||||||
|
|
||||||
use super::{media_type::MediaType, relation::Relation};
|
use super::{media_type::MediaType, relation::Relation};
|
||||||
|
|
||||||
|
/// Link element in OPDS.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename = "link")]
|
#[serde(rename = "link")]
|
||||||
pub struct Link {
|
pub struct Link {
|
||||||
|
/// Actual hyperlink.
|
||||||
#[serde(rename = "@href")]
|
#[serde(rename = "@href")]
|
||||||
pub href: String,
|
pub href: String,
|
||||||
|
/// Type of the target.
|
||||||
#[serde(rename = "@type")]
|
#[serde(rename = "@type")]
|
||||||
pub media_type: MediaType,
|
pub media_type: MediaType,
|
||||||
|
/// Relation of the target.
|
||||||
#[serde(rename = "@rel")]
|
#[serde(rename = "@rel")]
|
||||||
pub rel: Relation,
|
pub rel: Relation,
|
||||||
|
/// Optional link title.
|
||||||
#[serde(rename = "@title")]
|
#[serde(rename = "@title")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
/// Optional count (how many entries at the target).
|
||||||
#[serde(rename = "@thr:count")]
|
#[serde(rename = "@thr:count")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub count: Option<u64>,
|
pub count: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a format from a book into a link where it is downloadable.
|
||||||
impl From<(&Book, (&Format, &str))> for Link {
|
impl From<(&Book, (&Format, &str))> for Link {
|
||||||
fn from(value: (&Book, (&Format, &str))) -> Self {
|
fn from(value: (&Book, (&Format, &str))) -> Self {
|
||||||
let format = value.1 .0.clone();
|
let format = value.1 .0.clone();
|
||||||
let media_type: MediaType = format.into();
|
let media_type: MediaType = format.into();
|
||||||
Self {
|
Self {
|
||||||
href: format!("/book/{}/{}", value.0.id, value.1 .0),
|
href: format!("/book/{}/{}", value.0.data.id, value.1 .0),
|
||||||
media_type,
|
media_type,
|
||||||
rel: media_type.into(),
|
rel: media_type.into(),
|
||||||
title: Some(value.1 .0 .0.clone()),
|
title: Some(value.1 .0 .0.clone()),
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
|
//! Media types for OPDS elements.
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
use crate::data::book::Format;
|
use crate::data::book::Format;
|
||||||
|
|
||||||
|
/// Valid media types.
|
||||||
#[derive(Debug, Copy, Clone, SerializeDisplay)]
|
#[derive(Debug, Copy, Clone, SerializeDisplay)]
|
||||||
pub enum MediaType {
|
pub enum MediaType {
|
||||||
|
/// A link with this type is meant to acquire a certain thing, for example an entry.
|
||||||
Acquisition,
|
Acquisition,
|
||||||
Epub,
|
Epub,
|
||||||
Html,
|
Html,
|
||||||
Jpeg,
|
Jpeg,
|
||||||
|
/// A link with this type is meant for navigation around a feed.
|
||||||
Navigation,
|
Navigation,
|
||||||
Pdf,
|
Pdf,
|
||||||
Text,
|
Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert `epub` and `pdf` formats to their respective media type. Everything else is `Text`.
|
||||||
impl From<Format> for MediaType {
|
impl From<Format> for MediaType {
|
||||||
fn from(value: Format) -> Self {
|
fn from(value: Format) -> Self {
|
||||||
match value.0.as_ref() {
|
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 {
|
impl std::fmt::Display for MediaType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -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;
|
|
@ -1,10 +1,14 @@
|
|||||||
|
//! Relation data.
|
||||||
|
|
||||||
use serde_with::SerializeDisplay;
|
use serde_with::SerializeDisplay;
|
||||||
|
|
||||||
use super::media_type::MediaType;
|
use super::media_type::MediaType;
|
||||||
|
|
||||||
|
/// Types of relations for links.
|
||||||
#[derive(Debug, SerializeDisplay)]
|
#[derive(Debug, SerializeDisplay)]
|
||||||
pub enum Relation {
|
pub enum Relation {
|
||||||
Image,
|
Image,
|
||||||
|
/// Refer to the self feed.
|
||||||
Myself,
|
Myself,
|
||||||
Start,
|
Start,
|
||||||
Subsection,
|
Subsection,
|
||||||
@ -12,6 +16,9 @@ pub enum Relation {
|
|||||||
Acquisition,
|
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 {
|
impl From<MediaType> for Relation {
|
||||||
fn from(value: MediaType) -> Self {
|
fn from(value: MediaType) -> Self {
|
||||||
match value {
|
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 {
|
impl std::fmt::Display for Relation {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
//! Tera templates.
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
|
|
||||||
|
/// All tera templates, globally accessible.
|
||||||
pub static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
|
pub static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
|
||||||
let mut tera = Tera::default();
|
let mut tera = Tera::default();
|
||||||
tera.add_raw_templates(vec![
|
tera.add_raw_templates(vec![
|
||||||
|
@ -27,7 +27,11 @@
|
|||||||
</main>
|
</main>
|
||||||
<footer class="container">
|
<footer class="container">
|
||||||
<hr />
|
<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>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user