From a91fe9a0bbe206c427b78cc11344499925d05099 Mon Sep 17 00:00:00 2001 From: Sebastian Hugentobler Date: Mon, 6 May 2024 16:25:15 +0200 Subject: [PATCH] add series support --- calibre-db/src/calibre.rs | 22 ++++++++++++- calibre-db/src/data/author.rs | 14 ++------- calibre-db/src/data/book.rs | 27 +++++++++------- calibre-db/src/data/pagination.rs | 35 +++++++++++++++++---- calibre-db/src/data/series.rs | 20 ++++++++++-- rusty-library/src/handlers/books.rs | 9 +++--- rusty-library/src/handlers/series.rs | 34 ++++++++++++++++++-- rusty-library/src/handlers/series_single.rs | 35 +++++++++++++++++++++ rusty-library/src/main.rs | 8 ++++- rusty-library/src/templates.rs | 1 + rusty-library/templates/series.html | 21 +++++++++++++ 11 files changed, 183 insertions(+), 43 deletions(-) create mode 100644 rusty-library/src/handlers/series_single.rs create mode 100644 rusty-library/templates/series.html diff --git a/calibre-db/src/calibre.rs b/calibre-db/src/calibre.rs index 8798a9f..221a4cb 100644 --- a/calibre-db/src/calibre.rs +++ b/calibre-db/src/calibre.rs @@ -70,7 +70,7 @@ impl Calibre { &self, limit: u64, cursor: Option<&str>, - sort_order: SortOrder, + sort_order: &SortOrder, ) -> Result, DataStoreError> { let conn = self.pool.get()?; Series::multiple(&conn, limit, cursor, sort_order) @@ -81,6 +81,11 @@ impl Calibre { Series::book_series(&conn, id) } + pub fn series_books(&self, id: u64) -> Result, DataStoreError> { + let conn = self.pool.get()?; + Book::series_books(&conn, id) + } + pub fn has_previous_authors(&self, author_sort: &str) -> Result { let conn = self.pool.get()?; Author::has_previous_authors(&conn, author_sort) @@ -101,10 +106,25 @@ impl Calibre { Book::has_more_books(&conn, book_sort) } + pub fn has_previous_series(&self, series_sort: &str) -> Result { + let conn = self.pool.get()?; + Series::has_previous_series(&conn, series_sort) + } + + pub fn has_more_series(&self, series_sort: &str) -> Result { + let conn = self.pool.get()?; + Series::has_more_series(&conn, series_sort) + } + pub fn scalar_author(&self, id: u64) -> Result { let conn = self.pool.get()?; Author::scalar_author(&conn, id) } + + pub fn scalar_series(&self, id: u64) -> Result { + let conn = self.pool.get()?; + Series::scalar_series(&conn, id) + } } #[cfg(test)] diff --git a/calibre-db/src/data/author.rs b/calibre-db/src/data/author.rs index c48af6f..4b5d631 100644 --- a/calibre-db/src/data/author.rs +++ b/calibre-db/src/data/author.rs @@ -57,20 +57,10 @@ impl Author { conn: &Connection, sort_name: &str, ) -> Result { - let mut stmt = conn - .prepare("SELECT Count(1) FROM authors WHERE sort < (:sort_name) ORDER BY sort DESC")?; - let params = named_params! { ":sort_name": sort_name }; - let count: u64 = stmt.query_row(params, |x| x.get(0))?; - - Ok(count > 0) + Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::DESC) } pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result { - let mut stmt = conn - .prepare("SELECT Count(1) FROM authors WHERE sort > (:sort_name) ORDER BY sort ASC")?; - let params = named_params! { ":sort_name": sort_name }; - let count: u64 = stmt.query_row(params, |x| x.get(0))?; - - Ok(count > 0) + 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 d1ffe78..fde792b 100644 --- a/calibre-db/src/data/book.rs +++ b/calibre-db/src/data/book.rs @@ -57,6 +57,19 @@ impl Book { ) } + pub fn series_books(conn: &Connection, id: u64) -> Result, DataStoreError> { + let mut stmt = conn.prepare( + "SELECT books.id, books.title, books.sort, books.path FROM series \ + INNER JOIN books_series_link ON series.id = books_series_link.series \ + INNER JOIN books ON books.id = books_series_link.book \ + WHERE books_series_link.series = (:id) \ + ORDER BY books.series_index", + )?; + let params = named_params! { ":id": id }; + let iter = stmt.query_map(params, Self::from_row)?; + Ok(iter.filter_map(Result::ok).collect()) + } + pub fn recents(conn: &Connection, limit: u64) -> Result, DataStoreError> { let mut stmt = conn.prepare( "SELECT id, title, sort, path FROM books ORDER BY timestamp DESC LIMIT (:limit)", @@ -73,20 +86,10 @@ impl Book { } pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result { - let mut stmt = conn - .prepare("SELECT Count(1) FROM books WHERE sort < (:sort_title) ORDER BY sort DESC")?; - let params = named_params! { ":sort_title": sort_title}; - let count: u64 = stmt.query_row(params, |x| x.get(0))?; - - Ok(count > 0) + Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::DESC) } pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result { - let mut stmt = conn - .prepare("SELECT Count(1) FROM books WHERE sort > (:sort_title) ORDER BY sort ASC")?; - let params = named_params! { ":sort_title": sort_title}; - let count: u64 = stmt.query_row(params, |x| x.get(0))?; - - Ok(count > 0) + Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC) } } diff --git a/calibre-db/src/data/pagination.rs b/calibre-db/src/data/pagination.rs index 988dc17..d08449b 100644 --- a/calibre-db/src/data/pagination.rs +++ b/calibre-db/src/data/pagination.rs @@ -1,4 +1,4 @@ -use rusqlite::{Connection, Row, ToSql}; +use rusqlite::{named_params, Connection, Row, ToSql}; use serde::{Deserialize, Serialize}; use super::error::DataStoreError; @@ -31,6 +31,32 @@ impl<'a> Pagination<'a> { } } + fn sort_order_to_sql(sort_order: &SortOrder) -> String { + if *sort_order == SortOrder::ASC { + ">" + } else { + "<" + } + .to_string() + } + + pub fn has_prev_or_more( + conn: &Connection, + table: &str, + sort: &str, + sort_order: &SortOrder, + ) -> Result { + let comparison = Pagination::sort_order_to_sql(sort_order); + + let mut stmt = conn.prepare(&format!( + "SELECT Count(1) FROM {table} WHERE sort {comparison} (:sort) ORDER BY sort {sort_order:?}" + ))?; + let params = named_params! { ":sort": sort}; + let count: u64 = stmt.query_row(params, |x| x.get(0))?; + + Ok(count > 0) + } + pub fn paginate( &self, conn: &Connection, @@ -42,11 +68,8 @@ impl<'a> Pagination<'a> { F: FnMut(&Row<'_>) -> Result, { let cursor = self.cursor.unwrap_or(""); - let comparison = if self.sort_order == SortOrder::ASC { - ">" - } else { - "<" - }; + let comparison = Pagination::sort_order_to_sql(&self.sort_order); + let where_sql = if statement.ends_with("AND") { "" } else { diff --git a/calibre-db/src/data/series.rs b/calibre-db/src/data/series.rs index e81d46d..6f8fc67 100644 --- a/calibre-db/src/data/series.rs +++ b/calibre-db/src/data/series.rs @@ -26,17 +26,23 @@ impl Series { conn: &Connection, limit: u64, cursor: Option<&str>, - sort_order: SortOrder, + sort_order: &SortOrder, ) -> Result, DataStoreError> { - let pagination = Pagination::new("sort", cursor, limit, sort_order); + let pagination = Pagination::new("sort", cursor, limit, *sort_order); pagination.paginate( conn, - "SELECT id, title, sort, path FROM series", + "SELECT id, name, sort FROM series", &[], Self::from_row, ) } + 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)?) + } + pub fn book_series( conn: &Connection, book_id: u64, @@ -61,4 +67,12 @@ impl Series { Err(e) => Err(DataStoreError::SqliteError(e)), } } + + pub fn has_previous_series(conn: &Connection, sort_name: &str) -> Result { + Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::DESC) + } + + 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/rusty-library/src/handlers/books.rs b/rusty-library/src/handlers/books.rs index 8043715..e181f91 100644 --- a/rusty-library/src/handlers/books.rs +++ b/rusty-library/src/handlers/books.rs @@ -31,11 +31,10 @@ fn books( paginated::render( "books", || { - state.calibre.books(25, cursor, sort_order).map(|x| { - x.iter() - .filter_map(|y| Book::full_book(y, &state)) - .collect() - }) + state + .calibre + .books(25, cursor, sort_order) + .map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect()) }, |book| book.sort.clone(), |cursor| state.calibre.has_previous_books(cursor), diff --git a/rusty-library/src/handlers/series.rs b/rusty-library/src/handlers/series.rs index 9e900d9..50cb9e3 100644 --- a/rusty-library/src/handlers/series.rs +++ b/rusty-library/src/handlers/series.rs @@ -1,10 +1,38 @@ use std::sync::Arc; -use poem::{handler, web::Data}; +use calibre_db::data::pagination::SortOrder; +use poem::{ + handler, + web::{Data, Html, Path}, +}; use crate::app_state::AppState; +use super::paginated; + #[handler] -pub async fn handler(state: Data<&Arc>) -> Result { - Ok("series".to_string()) +pub async fn handler_init(state: Data<&Arc>) -> Result, poem::Error> { + series(&state, None, &SortOrder::ASC) +} + +#[handler] +pub async fn handler( + Path((cursor, sort_order)): Path<(String, SortOrder)>, + state: Data<&Arc>, +) -> Result, poem::Error> { + series(&state, Some(&cursor), &sort_order) +} + +fn series( + state: &Arc, + cursor: Option<&str>, + sort_order: &SortOrder, +) -> Result, poem::Error> { + paginated::render( + "series", + || state.calibre.series(25, cursor, sort_order), + |series| series.sort.clone(), + |cursor| state.calibre.has_previous_series(cursor), + |cursor| state.calibre.has_more_series(cursor), + ) } diff --git a/rusty-library/src/handlers/series_single.rs b/rusty-library/src/handlers/series_single.rs new file mode 100644 index 0000000..8e37e2d --- /dev/null +++ b/rusty-library/src/handlers/series_single.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use poem::{ + error::InternalServerError, + handler, + web::{Data, Html, Path}, +}; +use tera::Context; + +use crate::{ + app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES, +}; + +#[handler] +pub async fn handler( + id: Path, + state: Data<&Arc>, +) -> Result, poem::Error> { + let series = state.calibre.scalar_series(*id).map_err(SqliteError)?; + let books = state.calibre.series_books(*id).map_err(SqliteError)?; + let books = books + .iter() + .filter_map(|x| Book::full_book(x, &state)) + .collect::>(); + + let mut context = Context::new(); + context.insert("title", &series.name); + context.insert("nav", &series.name); + context.insert("books", &books); + + TEMPLATES + .render("book_list", &context) + .map_err(InternalServerError) + .map(Html) +} diff --git a/rusty-library/src/main.rs b/rusty-library/src/main.rs index c34c8b8..4bf0a8a 100644 --- a/rusty-library/src/main.rs +++ b/rusty-library/src/main.rs @@ -27,6 +27,7 @@ mod handlers { pub mod paginated; pub mod recents; pub mod series; + pub mod series_single; } mod templates; @@ -51,13 +52,18 @@ async fn main() -> Result<(), std::io::Error> { .at("/", get(handlers::recents::handler)) .at("/books", get(handlers::books::handler_init)) .at("/books/:cursor/:sort_order", get(handlers::books::handler)) + .at("/series", get(handlers::series::handler_init)) + .at( + "/series/:cursor/:sort_order", + get(handlers::series::handler), + ) + .at("/series/:id", get(handlers::series_single::handler)) .at("/authors", get(handlers::authors::handler_init)) .at("/authors/:id", get(handlers::author::handler)) .at( "/authors/:cursor/:sort_order", get(handlers::authors::handler), ) - .at("/series", get(handlers::series::handler)) .at("/cover/:id", get(handlers::cover::handler)) .at("/book/:id/:format", get(handlers::download::handler)) .nest("/static", EmbeddedFilesEndpoint::::new()) diff --git a/rusty-library/src/templates.rs b/rusty-library/src/templates.rs index 2f9b879..532f84c 100644 --- a/rusty-library/src/templates.rs +++ b/rusty-library/src/templates.rs @@ -9,6 +9,7 @@ pub static TEMPLATES: Lazy = Lazy::new(|| { ("authors", include_str!("../templates/authors.html")), ("book_list", include_str!("../templates/book_list.html")), ("books", include_str!("../templates/books.html")), + ("series", include_str!("../templates/series.html")), ]) .expect("failed to parse tera templates"); diff --git a/rusty-library/templates/series.html b/rusty-library/templates/series.html new file mode 100644 index 0000000..c719c9b --- /dev/null +++ b/rusty-library/templates/series.html @@ -0,0 +1,21 @@ +{% extends "base" %} +{% block title %} +{% if has_previous %} +← back +{% endif %} +{% if has_previous and has_more %}|{% endif%} + +{% if has_more %} +more → +{% endif %} +{% endblock title %} + +{% block content %} +
+ {% for s in series %} + +
{{ s.name }}
+
+ {% endfor %} +
+{% endblock content %}