basic recents view

This commit is contained in:
Sebastian Hugentobler 2024-05-02 18:10:29 +02:00
parent 65e17fc55b
commit 687c33829f
Signed by: shu
GPG key ID: BB32CF3CA052C2F0
20 changed files with 2156 additions and 20 deletions

View file

@ -0,0 +1,8 @@
use calibre_db::calibre::Calibre;
use crate::config::Config;
pub struct AppState {
pub calibre: Calibre,
pub config: Config,
}

View file

@ -0,0 +1,44 @@
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))
}
}

10
cops-web/src/cli.rs Normal file
View file

@ -0,0 +1,10 @@
use clap::Parser;
/// Simple odps server for calibre
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// Calibre library path
#[arg(last = true)]
pub library_path: String,
}

48
cops-web/src/config.rs Normal file
View file

@ -0,0 +1,48 @@
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::cli::Cli;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("no folder at {0}")]
LibraryPathNotFound(String),
#[error("no metadata.db in {0}")]
MetadataNotFound(String),
}
pub struct Config {
pub library_path: PathBuf,
pub metadata_path: PathBuf,
}
impl Config {
pub fn load(args: &Cli) -> Result<Self, ConfigError> {
let library_path = Path::new(&args.library_path).to_path_buf();
if !library_path.exists() {
let library_path = library_path
.as_os_str()
.to_str()
.unwrap_or("<failed to parse path>")
.to_string();
return Err(ConfigError::LibraryPathNotFound(library_path));
}
let metadata_path = library_path.join("metadata.db");
if !metadata_path.exists() {
let metadata_path = metadata_path
.as_os_str()
.to_str()
.unwrap_or("<failed to parse path>")
.to_string();
return Err(ConfigError::MetadataNotFound(metadata_path));
}
Ok(Self {
library_path,
metadata_path,
})
}
}

View file

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

View file

@ -0,0 +1,22 @@
use std::sync::Arc;
use poem::{
error::InternalServerError,
handler,
web::{Data, Html},
};
use tera::Context;
use crate::{app_state::AppState, templates::TEMPLATES};
#[handler]
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::Error> {
let recent_books = state.calibre.recent_books(50).unwrap();
let mut context = Context::new();
context.insert("books", &recent_books);
TEMPLATES
.render("recents", &context)
.map_err(InternalServerError)
.map(Html)
}

50
cops-web/src/main.rs Normal file
View file

@ -0,0 +1,50 @@
use std::sync::Arc;
use app_state::AppState;
use calibre_db::calibre::Calibre;
use clap::Parser;
use cli::Cli;
use config::Config;
use poem::{
endpoint::EmbeddedFilesEndpoint, get, listener::TcpListener, EndpointExt, Route, Server,
};
use rust_embed::RustEmbed;
mod app_state;
mod basic_auth;
mod cli;
mod config;
mod handlers {
pub mod cover;
pub mod recents;
}
mod templates;
#[derive(RustEmbed)]
#[folder = "static"]
pub struct Files;
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "debug");
}
tracing_subscriber::fmt::init();
let args = Cli::parse();
let config = Config::load(&args).expect("failed to load configuration");
let calibre = Calibre::load(&config.metadata_path).expect("failed to load calibre database");
let app_state = Arc::new(AppState { calibre, config });
let app = Route::new()
.at("/", get(handlers::recents::handler))
.at("/cover/:id", get(handlers::cover::handler))
.nest("/static", EmbeddedFilesEndpoint::<Files>::new())
.data(app_state);
Server::new(TcpListener::bind("[::]:3000"))
.name("cops-web")
.run(app)
.await
}

13
cops-web/src/templates.rs Normal file
View file

@ -0,0 +1,13 @@
use once_cell::sync::Lazy;
use tera::Tera;
pub static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("base", include_str!("../templates/base.html")),
("recents", include_str!("../templates/recents.html")),
])
.expect("failed to parse tera templates");
tera
});