implement downloads

This commit is contained in:
Sebastian Hugentobler 2024-05-06 10:40:34 +02:00
parent ac7b0e7e88
commit 5e9f49311e
Signed by: shu
GPG Key ID: BB32CF3CA052C2F0
8 changed files with 107 additions and 36 deletions

View File

@ -1,6 +1,10 @@
use std::{collections::HashMap, path::Path};
use calibre_db::data::{book::Book as DbBook, series::Series as DbSeries}; use calibre_db::data::{book::Book as DbBook, series::Series as DbSeries};
use serde::Serialize; use serde::Serialize;
use crate::app_state::AppState;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Book { pub struct Book {
pub id: u64, pub id: u64,
@ -9,6 +13,7 @@ pub struct Book {
pub path: String, pub path: String,
pub author: String, pub author: String,
pub series: Option<(String, f64)>, pub series: Option<(String, f64)>,
pub formats: HashMap<String, String>,
} }
impl Book { impl Book {
@ -16,6 +21,7 @@ impl Book {
db_book: &DbBook, db_book: &DbBook,
db_series: Option<(DbSeries, f64)>, db_series: Option<(DbSeries, f64)>,
author: &str, author: &str,
formats: HashMap<String, String>,
) -> Self { ) -> Self {
Self { Self {
id: db_book.id, id: db_book.id,
@ -24,6 +30,34 @@ impl Book {
path: db_book.path.clone(), path: db_book.path.clone(),
author: author.to_string(), author: author.to_string(),
series: db_series.map(|x| (x.0.name, x.1)), series: db_series.map(|x| (x.0.name, x.1)),
formats,
} }
} }
fn formats(book: &DbBook, library_path: &Path) -> HashMap<String, String> {
let book_path = library_path.join(&book.path);
let mut formats = HashMap::new();
for entry in book_path.read_dir().unwrap().flatten() {
if let Some(extension) = entry.path().extension() {
let format = match extension.to_string_lossy().to_string().as_str() {
"pdf" => Some("pdf".to_string()),
"epub" => Some("epub".to_string()),
_ => None,
};
if let Some(format) = format {
formats.insert(format, entry.file_name().to_string_lossy().to_string());
}
}
}
formats
}
pub fn full_book(book: &DbBook, state: &AppState) -> Option<Book> {
let formats = Book::formats(book, &state.config.library_path);
let author = state.calibre.book_author(book.id).ok()?;
let series = state.calibre.book_series(book.id).ok()?;
Some(Book::from_db_book(book, series, &author.name, formats))
}
} }

View File

@ -1,23 +1,25 @@
use std::{fs::File, io::Read, sync::Arc}; use std::{fs::File, io::Read, sync::Arc};
use poem::{ use poem::{
error::NotFoundError,
handler, handler,
web::{headers::ContentType, Data, Path, WithContentType}, web::{headers::ContentType, Data, Path, WithContentType},
IntoResponse, IntoResponse,
}; };
use crate::app_state::AppState; use crate::{app_state::AppState, handlers::error::SqliteError};
#[handler] #[handler]
pub async fn handler( pub async fn handler(
id: Path<u64>, id: Path<u64>,
state: Data<&Arc<AppState>>, state: Data<&Arc<AppState>>,
) -> Result<WithContentType<Vec<u8>>, poem::Error> { ) -> Result<WithContentType<Vec<u8>>, poem::Error> {
let book = state.calibre.scalar_book(*id).unwrap(); let book = state.calibre.scalar_book(*id).map_err(SqliteError)?;
let cover_path = state.config.library_path.join(book.path).join("cover.jpg"); let cover_path = state.config.library_path.join(book.path).join("cover.jpg");
let mut cover = File::open(cover_path).unwrap(); let mut cover = File::open(cover_path).map_err(|_| NotFoundError)?;
let mut data = Vec::new(); let mut data = Vec::new();
cover.read_to_end(&mut data).unwrap(); cover.read_to_end(&mut data).map_err(|_| NotFoundError)?;
Ok(data.with_content_type(ContentType::jpeg().to_string())) Ok(data.with_content_type(ContentType::jpeg().to_string()))
} }

View File

@ -0,0 +1,35 @@
use std::{fs::File, io::Read, sync::Arc};
use poem::{
error::NotFoundError,
handler,
web::{Data, Path, WithContentType, WithHeader},
IntoResponse,
};
use crate::{app_state::AppState, data::book::Book, handlers::error::SqliteError};
#[handler]
pub async fn handler(
Path((id, format)): Path<(u64, String)>,
state: Data<&Arc<AppState>>,
) -> Result<WithHeader<WithContentType<Vec<u8>>>, poem::Error> {
let book = state.calibre.scalar_book(id).map_err(SqliteError)?;
let book = Book::full_book(&book, &state).ok_or(NotFoundError)?;
let format: &str = format.as_str();
let file_name = book.formats.get(format).ok_or(NotFoundError)?;
let file_path = state.config.library_path.join(book.path).join(file_name);
let mut file = File::open(file_path).map_err(|_| NotFoundError)?;
let mut data = Vec::new();
file.read_to_end(&mut data).map_err(|_| NotFoundError)?;
let content_type = match format {
"pdf" => "application/pdf",
"epub" => "application/epub+zip",
_ => unreachable!(),
};
Ok(data
.with_content_type(content_type)
.with_header("Content-Disposition", format!("filename={file_name};")))
}

View File

@ -16,11 +16,7 @@ pub async fn handler(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::
let recent_books = state.calibre.recent_books(50).map_err(SqliteError)?; let recent_books = state.calibre.recent_books(50).map_err(SqliteError)?;
let recent_books = recent_books let recent_books = recent_books
.iter() .iter()
.filter_map(|x| { .filter_map(|x| Book::full_book(x, &state))
let author = state.calibre.book_author(x.id).ok()?;
let series = state.calibre.book_series(x.id).ok()?;
Some(Book::from_db_book(x, series, &author.name))
})
.collect::<Vec<Book>>(); .collect::<Vec<Book>>();
let mut context = Context::new(); let mut context = Context::new();

View File

@ -19,6 +19,7 @@ mod data {
} }
mod handlers { mod handlers {
pub mod cover; pub mod cover;
pub mod download;
pub mod error; pub mod error;
pub mod recents; pub mod recents;
} }
@ -44,6 +45,7 @@ async fn main() -> Result<(), std::io::Error> {
let app = Route::new() let app = Route::new()
.at("/", get(handlers::recents::handler)) .at("/", get(handlers::recents::handler))
.at("/cover/:id", get(handlers::cover::handler)) .at("/cover/:id", get(handlers::cover::handler))
.at("/book/:id/:format", get(handlers::download::handler))
.nest("/static", EmbeddedFilesEndpoint::<Files>::new()) .nest("/static", EmbeddedFilesEndpoint::<Files>::new())
.data(app_state); .data(app_state);

View File

@ -5,6 +5,7 @@ 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![
("base", include_str!("../templates/base.html")), ("base", include_str!("../templates/base.html")),
("book_card", include_str!("../templates/book_card.html")),
("recents", include_str!("../templates/recents.html")), ("recents", include_str!("../templates/recents.html")),
]) ])
.expect("failed to parse tera templates"); .expect("failed to parse tera templates");

View File

@ -0,0 +1,25 @@
<article class="book-card">
<header class="grid-item">
<hgroup>
<h5>{{ book.title }}</h5>
<p>{{ book.author }}</p>
</hgroup>
</header>
<img class="cover" src="/cover/{{ book.id }}" alt="book cover">
<footer>
<form>
<fieldset role="group">
<details class="dropdown">
<summary role="button" class="outline">
Download
</summary>
<ul>
{% for format, _ in book.formats %}
<li><a href="/book/{{ book.id }}/{{ format }}">{{ format }}</a></li>
{% endfor %}
</ul>
</details>
</fieldset>
</form>
</footer>
</article>

View File

@ -3,31 +3,7 @@
<h1>Recent Books</h1> <h1>Recent Books</h1>
<div class="grid-container"> <div class="grid-container">
{% for book in books %} {% for book in books %}
<article class="book-card"> {% include "book_card" %}
<header class="grid-item">
<hgroup>
<h5>{{ book.title }}</h5>
<!-- {% if book.series %}<p>{{ book.series.0 }} ({{ book.series.1 }})</p>{% endif %} -->
<p>{{ book.author }}</p>
</hgroup>
</header>
<img class="cover" src="/cover/{{ book.id }}" alt="book cover">
<footer>
<form>
<fieldset role="group">
<details class="dropdown">
<summary role="button" class="outline">
Download
</summary>
<ul>
<li><a href="#">Epub</a></li>
<li><a href="#">Pdf</a></li>
</ul>
</details>
</fieldset>
</form>
</footer>
</article>
{% endfor %} {% endfor %}
</div> </div>
{% endblock content %} {% endblock content %}