1053 lines
40 KiB
Rust
1053 lines
40 KiB
Rust
use axum::{
|
|
http::{HeaderValue, Method, StatusCode},
|
|
response::IntoResponse,
|
|
routing::{get,post,get_service},
|
|
Json, Router,
|
|
extract::{Extension, Path},
|
|
};
|
|
use clap::Parser;
|
|
use image;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
|
use std::str::FromStr;
|
|
use std::io;
|
|
use tower::ServiceBuilder;
|
|
use tower_http::services::ServeDir;
|
|
use tower_http::cors::{Any,CorsLayer};
|
|
use tower_http::trace::TraceLayer;
|
|
use booksman_search::BookMeili;
|
|
use booksman_search;
|
|
use booksman_orm::{
|
|
sea_orm::{Database, DatabaseConnection},
|
|
Mutation as MutationCore, Query as QueryCore
|
|
// BookAndMeta,
|
|
};
|
|
use meilisearch_sdk::client::Client;
|
|
//use itertools::Itertools;
|
|
use ::entity::entities::{book,book_author,book_person,book_place,book_subject,book_time,book_isbn,user};
|
|
use std::env;
|
|
use migration::{Migrator, MigratorTrait};
|
|
use chrono::Local;
|
|
use axum_login::{
|
|
axum_sessions::{async_session::MemoryStore as SessionMemoryStore, SessionLayer},
|
|
AuthLayer, RequireAuthorizationLayer,
|
|
};
|
|
use rand::Rng;
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct BookUI {
|
|
id: i32,
|
|
open_library_key: Option<String>,
|
|
title: String,
|
|
edition_count: Option<i32>,
|
|
first_publish_year: Option<i32>,
|
|
median_page_count: Option<i32>,
|
|
goodread_id: Option<String>,
|
|
description: Option<String>,
|
|
cover: Option<String>,
|
|
location: Option<String>,
|
|
time_added: Option<String>,
|
|
rating: Option<i32>,
|
|
comments: Option<String>,
|
|
author_name: Option<Vec<String>>,
|
|
person: Option<Vec<String>>,
|
|
place: Option<Vec<String>>,
|
|
subject: Option<Vec<String>>,
|
|
time: Option<Vec<String>>,
|
|
isbn: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct Docs {
|
|
key: String,
|
|
title: String,
|
|
edition_count: Option<i32>,
|
|
first_publish_year: Option<i32>,
|
|
number_of_pages_median: Option<i32>,
|
|
id_goodreads: Option<Vec<String>>,
|
|
description: Option<String>,
|
|
isbn: Option<Vec<String>>,
|
|
cover_i: Option<i32>,
|
|
cover_url: Option<String>,
|
|
author_name: Option<Vec<String>>,
|
|
person: Option<Vec<String>>,
|
|
place: Option<Vec<String>>,
|
|
subject: Option<Vec<String>>,
|
|
time: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct Books {
|
|
num_found: u32,
|
|
docs: Vec<Docs>,
|
|
}
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct PaginatedBookUIList {
|
|
num_pages: u32,
|
|
books: Vec<BookUI>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug)]
|
|
struct BookPageOL {
|
|
description: Option<DescriptionOL>,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
struct DescriptionOLValue {
|
|
value: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
#[serde(untagged)]
|
|
enum DescriptionOL {
|
|
DescriptionValueString(DescriptionOLValue),
|
|
DescriptionString(String),
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct BooksManyISBN {
|
|
#[serde(flatten)]
|
|
pub book_items: HashMap<String, BookISBNItem>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct BookISBNItem {
|
|
pub key: String,
|
|
pub title: String,
|
|
pub authors: Vec<Subject>,
|
|
pub identifiers: Option<Identifiers>,
|
|
pub publish_date: Option<String>,
|
|
pub subjects: Option<Vec<Subject>>,
|
|
pub subject_places: Option<Vec<Subject>>,
|
|
pub subject_people: Option<Vec<Subject>>,
|
|
pub subject_times: Option<Vec<Subject>>,
|
|
pub cover: Option<Cover>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Identifiers {
|
|
pub goodreads: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Subject {
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Cover {
|
|
pub medium: String,
|
|
}
|
|
|
|
|
|
impl Books {
|
|
fn set_all_cover_urls(&mut self) {
|
|
for book in self.docs.iter_mut() {
|
|
match book.cover_i {
|
|
Some(cover_i) => {
|
|
book.cover_url = Some(format!(
|
|
"https://covers.openlibrary.org/b/id/{}-M.jpg",
|
|
cover_i
|
|
))
|
|
}
|
|
None => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
impl BookUI {
|
|
async fn set_descriptions(&mut self) {
|
|
|
|
if self.open_library_key.is_some() {
|
|
if !(self.open_library_key.as_ref().unwrap().is_empty()) {
|
|
let query = format!("https://openlibrary.org/{}.json", self.open_library_key.as_ref().unwrap().clone());
|
|
let res = reqwest::get(query).await.expect("Unable to request");
|
|
let resjson = res.json::<BookPageOL>().await.expect("Unable to return value");
|
|
let description = resjson.description;
|
|
|
|
if !description.is_none() {
|
|
if let DescriptionOL::DescriptionString(desc_string) = description.clone().unwrap() {
|
|
self.description = Some(desc_string);
|
|
}
|
|
if let DescriptionOL::DescriptionValueString(desc_val) = description.clone().unwrap() {
|
|
self.description = Some(desc_val.value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
async fn handle_error(_err: io::Error) -> impl IntoResponse {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong...")
|
|
}
|
|
|
|
// Setup the command line interface with clap.
|
|
#[derive(Parser, Debug)]
|
|
#[clap(name = "server", about = "A server for our wasm project!")]
|
|
struct Opt {
|
|
/// set the log level
|
|
#[clap(short = 'l', long = "log", default_value = "debug")]
|
|
log_level: String,
|
|
|
|
/// set the listen addr
|
|
#[clap(short = 'a', long = "addr", default_value = "0.0.0.0")]
|
|
addr: String,
|
|
|
|
/// set the listen port
|
|
#[clap(short = 'p', long = "port", default_value = "8081")]
|
|
port: u16,
|
|
|
|
/// set the directory where static files are to be found
|
|
#[clap(long = "static-dir", default_value = "../dist")]
|
|
static_dir: String,
|
|
}
|
|
|
|
type AuthContext = axum_login::extractors::AuthContext<booksman_orm::AxumUser, booksman_orm::AxumUserStore>;
|
|
|
|
#[tokio::main]
|
|
pub async fn main() {
|
|
let opt = Opt::parse();
|
|
|
|
// Setup logging & RUST_LOG from args
|
|
if std::env::var("RUST_LOG").is_err() {
|
|
std::env::set_var("RUST_LOG", format!("{},hyper=info,mio=info", opt.log_level))
|
|
}
|
|
// enable console logging
|
|
tracing_subscriber::fmt::init();
|
|
|
|
|
|
dotenvy::dotenv().ok();
|
|
let images_dir = env::var("IMAGES_DIR").expect("IMAGES_DIR is not set in .env file");
|
|
let cors_url = env::var("CORS_URL").expect("CORS_URL is not set in .env file");
|
|
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
|
|
let meili_url = env::var("MEILI_URL").expect("MEILI_URL is not set in .env file");
|
|
let meili_key = env::var("MEILI_KEY").expect("MEILI_KEY is not set in .env file");
|
|
|
|
let secret = rand::thread_rng().gen::<[u8; 64]>();
|
|
|
|
let conn = Database::connect(db_url)
|
|
.await
|
|
.expect("Database connection failed");
|
|
// Apply all pending migrations
|
|
Migrator::up(&conn, None).await.unwrap();
|
|
|
|
|
|
let session_store = SessionMemoryStore::new();
|
|
let session_layer = SessionLayer::new(session_store, &secret).with_secure(false);
|
|
|
|
|
|
let user_store = booksman_orm::AxumUserStore::new(&conn);
|
|
let auth_layer: AuthLayer<booksman_orm::AxumUserStore, booksman_orm::AxumUser> = AuthLayer::new(user_store, &secret);
|
|
|
|
let meili_client = Client::new(meili_url, meili_key);
|
|
|
|
|
|
let app = Router::new()
|
|
.route("/api/create_by_isbn", get(create_by_isbn))
|
|
.route("/api/create", post(create_book))
|
|
.route("/api/update", post(update_book))
|
|
.route("/api/delete/:id", get(delete_book))
|
|
.route("/api/authentication_check", get(authentication_check))
|
|
.route_layer(RequireAuthorizationLayer::<booksman_orm::AxumUser>::login())
|
|
.route("/api/list", get(list_book))
|
|
.route("/api/list_search", get(list_search_book))
|
|
.route("/api/list_users", get(list_users))
|
|
.route("/api/search_openlibrary", get(search_openlibrary))
|
|
.route("/api/login", post(login_handler))
|
|
.route("/api/register", post(register_handler))
|
|
.route("/api/logout", post(logout_handler))
|
|
.nest_service("/images", get_service(ServeDir::new(images_dir)).handle_error(handle_error))
|
|
.nest_service("/assets", get_service(ServeDir::new(opt.static_dir)).handle_error(handle_error))
|
|
// .merge(SpaRouter::new("/assets", opt.static_dir))
|
|
.layer(auth_layer)
|
|
.layer(session_layer)
|
|
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
|
.layer(Extension(conn))
|
|
.layer(Extension(meili_client))
|
|
.layer(
|
|
// see https://docs.rs/tower-http/latest/tower_http/cors/index.html
|
|
// for more details
|
|
//
|
|
// pay attention that for some request types like posting content-type: application/json
|
|
// it is required to add ".allow_headers([http::header::CONTENT_TYPE])"
|
|
// or see this issue https://github.com/tokio-rs/axum/issues/849
|
|
CorsLayer::new()
|
|
.allow_methods([Method::GET, Method::POST])
|
|
.allow_origin(cors_url.parse::<HeaderValue>().unwrap())
|
|
.allow_headers(Any),
|
|
);
|
|
|
|
let sock_addr = SocketAddr::from((
|
|
IpAddr::from_str(opt.addr.as_str()).unwrap_or(IpAddr::V6(Ipv6Addr::LOCALHOST)),
|
|
opt.port,
|
|
));
|
|
|
|
log::info!("listening on http://{}", sock_addr);
|
|
|
|
axum::Server::bind(&sock_addr)
|
|
.serve(app.into_make_service())
|
|
.await
|
|
.expect("Unable to start server");
|
|
}
|
|
|
|
|
|
//#[axum_macros::debug_handler]
|
|
async fn register_handler(Extension(ref conn): Extension<DatabaseConnection>,
|
|
Json(user_sent): Json<booksman_orm::AxumUser>) -> impl IntoResponse {
|
|
// add to db
|
|
let user: user::Model = user::Model{
|
|
id: user_sent.id,
|
|
user_name: Some(user_sent.clone().name),
|
|
password_hash: user_sent.clone().password_hash,
|
|
};
|
|
let _created_user = MutationCore::create_user(conn, user).await.expect("Failed to create user");
|
|
return "success";
|
|
}
|
|
|
|
|
|
async fn list_users(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
) -> impl IntoResponse {
|
|
|
|
let usersmodels : Vec<user::Model> = QueryCore::list_all_users(&conn).await.unwrap_or(Vec::new());
|
|
let mut users : Vec<booksman_orm::AxumUser> = Vec::new();
|
|
for usermodel in usersmodels.iter() {
|
|
let user = booksman_orm::AxumUser{
|
|
id: usermodel.id,
|
|
name: usermodel.user_name.clone().unwrap().clone(),
|
|
password_hash : "".to_string(),
|
|
};
|
|
users.push(user);
|
|
}
|
|
return Json(users);
|
|
}
|
|
async fn login_handler(mut auth: AuthContext, Extension(ref conn): Extension<DatabaseConnection>, Json(user_sent): Json<booksman_orm::AxumUser>) -> impl IntoResponse {
|
|
let userdb : user::Model = QueryCore::find_userid_by_name(conn, user_sent.name.clone()).await.unwrap().unwrap();
|
|
let userid = userdb.id;
|
|
let corrected_user = booksman_orm::AxumUser {
|
|
id: userid,
|
|
name: user_sent.name.clone(),
|
|
password_hash: user_sent.password_hash.clone()
|
|
};
|
|
auth.login(&corrected_user).await.unwrap();
|
|
return "success";
|
|
}
|
|
|
|
//
|
|
|
|
async fn logout_handler(mut auth: AuthContext) {
|
|
dbg!("Logging out user: {}", &auth.current_user);
|
|
auth.logout().await;
|
|
}
|
|
|
|
//https://openlibrary.org/api/books?bibkeys=ISBN:9780980200447&jscmd=data&format=json
|
|
async fn authentication_check(
|
|
Extension(user): Extension<booksman_orm::AxumUser>,
|
|
) -> impl IntoResponse {
|
|
return true;
|
|
}
|
|
|
|
|
|
//https://openlibrary.org/api/books?bibkeys=ISBN:9780980200447&jscmd=data&format=json
|
|
async fn create_by_isbn(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Extension(ref meili_client): Extension<Client>,
|
|
Extension(user): Extension<booksman_orm::AxumUser>,
|
|
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
|
|
dotenvy::dotenv().ok();
|
|
let images_dir = env::var("IMAGES_DIR").expect("IMAGES_DIR is not set in .env file");
|
|
let userid = user.id;
|
|
|
|
print!("Get items with query params: {:?}\n", params);
|
|
let isbns = params.get("isbns").unwrap();
|
|
let splitisbns : Vec<String> = isbns.split(",").map(|s| format!("ISBN:{}",s.to_string())).collect();
|
|
let joinedisbns = splitisbns.join(",");
|
|
let query = format!("https://openlibrary.org/api/books?bibkeys={}&jscmd=data&format=json", joinedisbns);
|
|
let res = reqwest::get(query).await.expect("Unable to request");
|
|
let resjson: BooksManyISBN = res.json::<BooksManyISBN>().await.expect("Unable to return value");
|
|
for isbnstring in splitisbns.iter() {
|
|
let bookfound = resjson.book_items.get(isbnstring);
|
|
if !bookfound.clone().is_none() {
|
|
let mut cover_id = "".to_string();
|
|
if !bookfound.unwrap().cover.is_none() {
|
|
let coverurl = bookfound.clone().unwrap().cover.clone().unwrap().medium.clone();
|
|
let img_bytes = reqwest::get(coverurl.clone()).await.unwrap().bytes().await.unwrap();
|
|
let image = image::load_from_memory(&img_bytes).unwrap();
|
|
let temp_cover = coverurl.clone();
|
|
let img_id = temp_cover.split("/").last().unwrap();
|
|
image.save(format!("{}/{}",images_dir,img_id)).expect("Failed to save image");
|
|
cover_id = img_id.to_string();
|
|
|
|
}
|
|
let mut goodread_id = "".to_string();
|
|
if !bookfound.unwrap().identifiers.is_none() {
|
|
let goodread_identifier = bookfound.clone().unwrap().identifiers.clone().unwrap().goodreads;
|
|
if !goodread_identifier.is_none() {
|
|
goodread_id = goodread_identifier.unwrap().get(0).unwrap().to_string()
|
|
}
|
|
}
|
|
//let mut authors_name = Some(vec!["".to_string()]);
|
|
//if !bookfound.unwrap().authors.is_none() {
|
|
let authors_name = Some(bookfound.clone().unwrap().authors.iter().map(|s| s.name.clone()).collect());
|
|
//} else {
|
|
// authors_name = None
|
|
//}
|
|
let persons : Option<Vec<String>>;
|
|
//= Some(vec!["".to_string()]);
|
|
if !bookfound.unwrap().subject_people.is_none() {
|
|
persons = Some(bookfound.clone().unwrap().subject_people.clone().unwrap().iter().map(|s| s.name.clone()).collect());
|
|
} else {
|
|
persons = None;
|
|
}
|
|
let places : Option<Vec<String>>;
|
|
if !bookfound.unwrap().subject_places.is_none() {
|
|
places = Some(bookfound.clone().unwrap().subject_places.clone().unwrap().iter().map(|s| s.name.clone()).collect());
|
|
} else {
|
|
places = None;
|
|
}
|
|
let subjects : Option<Vec<String>>;
|
|
if !bookfound.unwrap().subjects.is_none() {
|
|
subjects = Some(bookfound.clone().unwrap().subjects.clone().unwrap().iter().map(|s| s.name.clone()).collect());
|
|
} else {
|
|
subjects = None;
|
|
}
|
|
let times : Option<Vec<String>>;
|
|
if !bookfound.unwrap().subject_times.is_none() {
|
|
times = Some(bookfound.clone().unwrap().subject_times.clone().unwrap().iter().map(|s| s.name.clone()).collect());
|
|
} else {
|
|
times = None;
|
|
}
|
|
|
|
let mut doc_sent = BookUI{
|
|
id: 1,
|
|
open_library_key: Some(bookfound.clone().unwrap().key.clone()),
|
|
title: bookfound.clone().unwrap().title.clone(),
|
|
edition_count: None,
|
|
first_publish_year: None,
|
|
median_page_count: None,
|
|
goodread_id: Some(goodread_id),
|
|
description: None,
|
|
cover: Some(cover_id.clone()),
|
|
location: None,
|
|
time_added: None,
|
|
rating: None,
|
|
comments: Some("From openlibrary".to_string()),
|
|
author_name: authors_name,
|
|
person: persons,
|
|
place: places,
|
|
subject: subjects,
|
|
time: times,
|
|
isbn: Some(vec![isbnstring.split("ISBN:").last().unwrap().to_string()])
|
|
};
|
|
doc_sent.set_descriptions().await;
|
|
let book: book::Model = book::Model{
|
|
open_library_key: doc_sent.open_library_key.to_owned(),
|
|
title: (doc_sent.title.to_owned()),
|
|
edition_count: doc_sent.edition_count.to_owned(),
|
|
first_publish_year: (doc_sent.first_publish_year.to_owned()),
|
|
median_page_count: (doc_sent.median_page_count.to_owned()),
|
|
goodread_id: doc_sent.goodread_id.to_owned(),
|
|
description: doc_sent.description.to_owned(),
|
|
comments: doc_sent.comments.to_owned(),
|
|
cover: Some(cover_id.to_owned()),
|
|
rating: doc_sent.rating.to_owned(),
|
|
time_added: doc_sent.time_added.to_owned(),
|
|
id: 1,
|
|
location: doc_sent.location.to_owned(),
|
|
user_id: userid,
|
|
};
|
|
let created_book = MutationCore::create_book(conn, book)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
for author_name in doc_sent.author_name.as_ref().unwrap().iter() {
|
|
let record : book_author::Model = book_author::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
author_name: author_name.to_owned(),
|
|
};
|
|
MutationCore::create_book_author(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
}
|
|
for person in doc_sent.person.as_ref().unwrap().iter() {
|
|
let record : book_person::Model = book_person::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
person: person.to_owned(),
|
|
};
|
|
MutationCore::create_book_person(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for place in doc_sent.place.as_ref().unwrap().iter() {
|
|
let record : book_place::Model = book_place::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
place: place.to_owned(),
|
|
};
|
|
MutationCore::create_book_place(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for subject in doc_sent.subject.as_ref().unwrap().iter() {
|
|
let record : book_subject::Model = book_subject::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
subject: subject.to_owned(),
|
|
};
|
|
MutationCore::create_book_subject(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for time in doc_sent.time.as_ref().unwrap().iter() {
|
|
let record : book_time::Model = book_time::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
time: time.to_owned(),
|
|
};
|
|
MutationCore::create_book_time(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for isbn in doc_sent.isbn.as_ref().unwrap().iter() {
|
|
let record : book_isbn::Model = book_isbn::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
isbn: isbn.to_owned(),
|
|
};
|
|
MutationCore::create_book_isbn(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
let book_meili = BookMeili{
|
|
id: doc_sent.id,
|
|
open_library_key: doc_sent.open_library_key.unwrap_or("".to_string()),
|
|
title: doc_sent.title,
|
|
edition_count: doc_sent.edition_count.unwrap_or(0),
|
|
first_publish_year: doc_sent.first_publish_year.unwrap_or(0),
|
|
median_page_count: doc_sent.median_page_count.unwrap_or(0),
|
|
goodread_id: doc_sent.goodread_id.unwrap_or("".to_string()),
|
|
description: doc_sent.description.unwrap_or("".to_string()),
|
|
cover: doc_sent.cover.unwrap_or("".to_string()),
|
|
location: doc_sent.location.unwrap_or("".to_string()),
|
|
time_added: doc_sent.time_added.unwrap_or("".to_string()),
|
|
rating: doc_sent.rating.unwrap_or(0),
|
|
comments: doc_sent.comments.unwrap_or("".to_string()),
|
|
author_name: doc_sent.author_name.unwrap_or(vec!["".to_string()]),
|
|
person: doc_sent.person.unwrap_or(vec!["".to_string()]),
|
|
place: doc_sent.place.unwrap_or(vec!["".to_string()]),
|
|
subject: doc_sent.subject.unwrap_or(vec!["".to_string()]),
|
|
time: doc_sent.time.unwrap_or(vec!["".to_string()]),
|
|
isbn: doc_sent.isbn.unwrap_or(vec!["".to_string()]),
|
|
};
|
|
booksman_search::create_or_update_book(book_meili, userid, meili_client).await;
|
|
} else {
|
|
println!("ISBN Not found : {}",isbnstring);
|
|
}
|
|
}
|
|
return "Done";
|
|
}
|
|
|
|
|
|
async fn search_openlibrary(
|
|
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
print!("Get items with query params: {:?}\n", params);
|
|
let search = params.get("search").unwrap();
|
|
let query = format!("https://openlibrary.org/search.json?q={}", search);
|
|
let res = reqwest::get(query).await.expect("Unable to request");
|
|
let mut resjson = res.json::<Books>().await.expect("Unable to return value");
|
|
resjson.docs.truncate(12);
|
|
resjson.set_all_cover_urls();
|
|
//resjson.set_all_descriptions().await;
|
|
print!("Search token {:?}\n", search);
|
|
let mut vec = Vec::with_capacity(12);
|
|
for i in 0..12 as usize {
|
|
let doc = resjson.docs[i].clone();
|
|
let mut goodread = "".to_string();
|
|
if doc.id_goodreads.is_some() {
|
|
goodread = doc.id_goodreads.unwrap().get(0).unwrap().to_string();
|
|
}
|
|
vec.push(
|
|
BookUI{
|
|
id: -((i+1) as i32),
|
|
open_library_key: Some(doc.key),
|
|
title: doc.title,
|
|
edition_count: doc.edition_count,
|
|
first_publish_year: doc.first_publish_year,
|
|
median_page_count: doc.number_of_pages_median,
|
|
goodread_id: Some(goodread),
|
|
description: doc.description,
|
|
cover: doc.cover_url,
|
|
location: None,
|
|
time_added: None,
|
|
rating: None,
|
|
comments: Some("From openlibrary".to_string()),
|
|
author_name: doc.author_name,
|
|
person: doc.person,
|
|
place: doc.place,
|
|
subject: doc.subject,
|
|
time: doc.time,
|
|
isbn: doc.isbn
|
|
}
|
|
);
|
|
}
|
|
return Json(vec);
|
|
}
|
|
|
|
async fn delete_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Extension(ref meili_client): Extension<Client>,
|
|
Extension(user): Extension<booksman_orm::AxumUser>,
|
|
Path(id): Path<i32>,
|
|
) -> impl IntoResponse {
|
|
MutationCore::delete_book(conn, id)
|
|
.await
|
|
.expect("could not delete book");
|
|
let userid = user.id;
|
|
booksman_search::delete_book(id, userid, meili_client).await;
|
|
"success"
|
|
}
|
|
|
|
async fn list_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
|
|
|
|
dotenvy::dotenv().ok();
|
|
let backend_url = env::var("BACKEND_URL").expect("BACKEND_URL is not set in .env file");
|
|
|
|
let page: usize = params.get("page").unwrap().parse().unwrap();
|
|
let userid: usize = params.get("userid").unwrap().parse().unwrap();
|
|
let sort: String = params.get("sort").unwrap().to_string();
|
|
|
|
let books = QueryCore::find_books_plus_meta_in_page(conn,page,userid,12, sort)
|
|
.await
|
|
.expect("could not list books");
|
|
|
|
let mut resbooks: Vec<BookUI> = Vec::with_capacity(12);
|
|
|
|
for bookandmeta in books.0.into_iter() {
|
|
let mut cover = bookandmeta.clone().book.cover;
|
|
if !cover.is_none() {
|
|
cover = Some(format!("{}/images/{}",backend_url, cover.unwrap()));
|
|
} else {
|
|
cover = Some(format!("{}/images/placeholder.jpg",backend_url));
|
|
}
|
|
let bookui = BookUI{
|
|
id: bookandmeta.clone().book.id,
|
|
title: bookandmeta.clone().book.title,
|
|
open_library_key: bookandmeta.clone().book.open_library_key,
|
|
edition_count: bookandmeta.clone().book.edition_count,
|
|
first_publish_year: bookandmeta.clone().book.first_publish_year,
|
|
median_page_count: bookandmeta.clone().book.median_page_count,
|
|
goodread_id: bookandmeta.clone().book.goodread_id,
|
|
description: bookandmeta.clone().book.description,
|
|
cover: cover.clone(),
|
|
location: bookandmeta.clone().book.location,
|
|
time_added: bookandmeta.clone().book.time_added,
|
|
rating: bookandmeta.clone().book.rating,
|
|
comments: bookandmeta.clone().book.comments,
|
|
author_name: Some(bookandmeta.clone().authors.into_iter().map(|u| u.author_name).collect() ),
|
|
person: Some(bookandmeta.clone().persons.into_iter().map(|u| u.person).collect()),
|
|
place: Some(bookandmeta.clone().places.into_iter().map(|u| u.place).collect()),
|
|
subject: Some(bookandmeta.clone().subjects.into_iter().map(|u| u.subject).collect()),
|
|
time: Some(bookandmeta.clone().times.into_iter().map(|u| u.time).collect()),
|
|
isbn: Some(bookandmeta.clone().isbns.into_iter().map(|u| u.isbn).collect()),
|
|
};
|
|
resbooks.push(bookui);
|
|
}
|
|
|
|
let res = PaginatedBookUIList{
|
|
num_pages: books.1 as u32,
|
|
books: resbooks
|
|
};
|
|
return Json(res);
|
|
// "success"
|
|
}
|
|
|
|
|
|
async fn list_search_book(
|
|
Extension(ref meili_client): Extension<Client>,
|
|
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
|
) -> impl IntoResponse {
|
|
let page: usize = params.get("page").unwrap().parse().unwrap();
|
|
let search = params.get("search").unwrap();
|
|
let userid: i32 = params.get("userid").unwrap().parse().unwrap();
|
|
|
|
dotenvy::dotenv().ok();
|
|
let backend_url = env::var("BACKEND_URL").expect("BACKEND_URL is not set in .env file");
|
|
|
|
|
|
let books = booksman_search::search_book(search, page, userid, meili_client)
|
|
.await
|
|
.expect("could not list books");
|
|
|
|
let mut resbooks: Vec<BookUI> = Vec::with_capacity(12);
|
|
|
|
for bookmeili in books.0.into_iter() {
|
|
let mut cover = bookmeili.clone().cover;
|
|
if cover!="".to_string() {
|
|
cover = format!("{}/images/{}",backend_url,cover);
|
|
} else {
|
|
cover = format!("{}/images/placeholder.jpg",backend_url);
|
|
}
|
|
let bookui = BookUI{
|
|
id: bookmeili.id,
|
|
title: bookmeili.title,
|
|
open_library_key: Some(bookmeili.open_library_key),
|
|
edition_count: Some(bookmeili.edition_count),
|
|
first_publish_year: Some(bookmeili.first_publish_year),
|
|
median_page_count: Some(bookmeili.median_page_count),
|
|
goodread_id: Some(bookmeili.goodread_id),
|
|
description: Some(bookmeili.description),
|
|
cover: Some(cover.clone()),
|
|
location: Some(bookmeili.location),
|
|
time_added: Some(bookmeili.time_added),
|
|
rating: Some(bookmeili.rating),
|
|
comments: Some(bookmeili.comments),
|
|
author_name: Some(bookmeili.author_name),
|
|
person: Some(bookmeili.person),
|
|
place: Some(bookmeili.place),
|
|
subject: Some(bookmeili.subject),
|
|
time: Some(bookmeili.time),
|
|
isbn: Some(bookmeili.isbn),
|
|
};
|
|
resbooks.push(bookui);
|
|
}
|
|
|
|
let res = PaginatedBookUIList{
|
|
num_pages: (books.1/12+1) as u32,
|
|
books: resbooks
|
|
};
|
|
return Json(res);
|
|
// "success"
|
|
}
|
|
|
|
|
|
async fn create_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Extension(ref meili_client): Extension<Client>,
|
|
Extension(user): Extension<booksman_orm::AxumUser>,
|
|
Json(doc_sent_orig): Json<BookUI>,
|
|
) -> impl IntoResponse {
|
|
println!("Creating book");
|
|
|
|
dotenvy::dotenv().ok();
|
|
let images_dir = env::var("IMAGES_DIR").expect("IMAGES_DIR is not set in .env file");
|
|
let userid = user.id;
|
|
let mut doc_sent = doc_sent_orig.clone();
|
|
let mut cover = doc_sent.cover.clone();
|
|
if doc_sent_orig.description.is_none() {
|
|
doc_sent.set_descriptions().await;
|
|
} else {
|
|
if doc_sent_orig.description.unwrap() == "".to_string() {
|
|
doc_sent.set_descriptions().await;
|
|
}
|
|
}
|
|
|
|
if !doc_sent.cover.is_none() {
|
|
if !doc_sent.cover.clone().unwrap().is_empty() {
|
|
let img_bytes = reqwest::get(cover.unwrap()).await.unwrap().bytes().await.unwrap();
|
|
let image = image::load_from_memory(&img_bytes).unwrap();
|
|
let temp_cover = doc_sent.cover.clone().unwrap();
|
|
let img_id = format!("{}{}",Local::now().format("%Y-%m-%d-%H-%M-%S"),temp_cover.split("/").last().unwrap());
|
|
image.save(format!("{}/{}",images_dir,img_id)).expect("Failed to save image");
|
|
cover = Some(img_id.to_string());
|
|
}
|
|
}
|
|
|
|
let book: book::Model = book::Model{
|
|
open_library_key: doc_sent.open_library_key.to_owned(),
|
|
title: (doc_sent.title.to_owned()),
|
|
edition_count: doc_sent.edition_count.to_owned(),
|
|
first_publish_year: (doc_sent.first_publish_year.to_owned()),
|
|
median_page_count: (doc_sent.median_page_count.to_owned()),
|
|
goodread_id: doc_sent.goodread_id.to_owned(),
|
|
description: doc_sent.description.to_owned(),
|
|
comments: doc_sent.comments.to_owned(),
|
|
cover: cover.to_owned(),
|
|
rating: doc_sent.rating.to_owned(),
|
|
time_added: doc_sent.time_added.to_owned(),
|
|
id: 1,
|
|
location: doc_sent.location.to_owned(),
|
|
user_id: userid,
|
|
};
|
|
let created_book = MutationCore::create_book(conn, book)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
for author_name in doc_sent.author_name.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_author::Model = book_author::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
author_name: author_name.to_owned(),
|
|
};
|
|
MutationCore::create_book_author(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
}
|
|
for person in doc_sent.person.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_person::Model = book_person::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
person: person.to_owned(),
|
|
};
|
|
MutationCore::create_book_person(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for place in doc_sent.place.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_place::Model = book_place::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
place: place.to_owned(),
|
|
};
|
|
MutationCore::create_book_place(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for subject in doc_sent.subject.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_subject::Model = book_subject::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
subject: subject.to_owned(),
|
|
};
|
|
MutationCore::create_book_subject(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for time in doc_sent.time.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_time::Model = book_time::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
time: time.to_owned(),
|
|
};
|
|
MutationCore::create_book_time(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for isbn in doc_sent.isbn.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_isbn::Model = book_isbn::Model{
|
|
id: 1,
|
|
book_id: (created_book.last_insert_id),
|
|
isbn: isbn.to_owned(),
|
|
};
|
|
MutationCore::create_book_isbn(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
let book_meili = BookMeili{
|
|
id: doc_sent.id,
|
|
open_library_key: doc_sent.open_library_key.unwrap_or("".to_string()),
|
|
title: doc_sent.title,
|
|
edition_count: doc_sent.edition_count.unwrap_or(0),
|
|
first_publish_year: doc_sent.first_publish_year.unwrap_or(0),
|
|
median_page_count: doc_sent.median_page_count.unwrap_or(0),
|
|
goodread_id: doc_sent.goodread_id.unwrap_or("".to_string()),
|
|
description: doc_sent.description.unwrap_or("".to_string()),
|
|
cover: cover.unwrap_or("".to_string()),
|
|
location: doc_sent.location.unwrap_or("".to_string()),
|
|
time_added: doc_sent.time_added.unwrap_or("".to_string()),
|
|
rating: doc_sent.rating.unwrap_or(0),
|
|
comments: doc_sent.comments.unwrap_or("".to_string()),
|
|
author_name: doc_sent.author_name.unwrap_or(vec!["".to_string()]),
|
|
person: doc_sent.person.unwrap_or(vec!["".to_string()]),
|
|
place: doc_sent.place.unwrap_or(vec!["".to_string()]),
|
|
subject: doc_sent.subject.unwrap_or(vec!["".to_string()]),
|
|
time: doc_sent.time.unwrap_or(vec!["".to_string()]),
|
|
isbn: doc_sent.isbn.unwrap_or(vec!["".to_string()]),
|
|
};
|
|
booksman_search::create_or_update_book(book_meili, userid, meili_client).await;
|
|
|
|
|
|
"success"
|
|
}
|
|
|
|
|
|
|
|
async fn update_book(
|
|
Extension(ref conn): Extension<DatabaseConnection>,
|
|
Extension(ref meili_client): Extension<Client>,
|
|
Extension(user): Extension<booksman_orm::AxumUser>,
|
|
Json(doc_sent): Json<BookUI>,
|
|
) -> impl IntoResponse {
|
|
println!("Updating book");
|
|
|
|
dotenvy::dotenv().ok();
|
|
let images_dir = env::var("IMAGES_DIR").expect("IMAGES_DIR is not set in .env file");
|
|
let backend_url = env::var("BACKEND_URL").expect("BACKEND_URL is not set in .env file");
|
|
let userid = user.id.clone();
|
|
|
|
let mut cover = doc_sent.cover.clone();
|
|
if !doc_sent.cover.is_none() {
|
|
if !doc_sent.cover.clone().unwrap().is_empty() {
|
|
if doc_sent.cover.clone().unwrap().contains(&backend_url) {
|
|
cover = Some(doc_sent.cover.clone().unwrap().split("/").last().unwrap().to_string());
|
|
}
|
|
else {
|
|
let img_bytes = reqwest::get(cover.unwrap()).await.unwrap().bytes().await.unwrap();
|
|
let image = image::load_from_memory(&img_bytes).unwrap();
|
|
let temp_cover = doc_sent.cover.clone().unwrap();
|
|
let img_id = format!("{}{}",Local::now().format("%Y-%m-%d-%H-%M-%S"),temp_cover.split("/").last().unwrap());
|
|
image.save(format!("{}/{}",images_dir,img_id)).expect("Failed to save image");
|
|
cover = Some(img_id.to_string());
|
|
}
|
|
}
|
|
}
|
|
let book: book::Model = book::Model{
|
|
open_library_key: doc_sent.open_library_key.to_owned(),
|
|
title: (doc_sent.title.to_owned()),
|
|
edition_count: doc_sent.edition_count.to_owned(),
|
|
first_publish_year: (doc_sent.first_publish_year.to_owned()),
|
|
median_page_count: (doc_sent.median_page_count.to_owned()),
|
|
goodread_id: doc_sent.goodread_id.to_owned(),
|
|
description: doc_sent.description.to_owned(),
|
|
comments: doc_sent.comments.to_owned(),
|
|
cover: cover.to_owned(),
|
|
rating: doc_sent.rating.to_owned(),
|
|
time_added: doc_sent.time_added.to_owned(),
|
|
id: doc_sent.id.to_owned(),
|
|
location: doc_sent.location.to_owned(),
|
|
user_id: userid,
|
|
};
|
|
MutationCore::update_book_by_id(conn, book.id, book)
|
|
.await
|
|
.expect("could not update book");
|
|
|
|
MutationCore::delete_book_author(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book authors while updating");
|
|
MutationCore::delete_book_person(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book persons while updating");
|
|
|
|
MutationCore::delete_book_place(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book places while updating");
|
|
|
|
MutationCore::delete_book_subject(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book subjects while updating");
|
|
|
|
MutationCore::delete_book_time(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book times while updating");
|
|
|
|
MutationCore::delete_book_isbn(conn, doc_sent.id)
|
|
.await
|
|
.expect("could not delete book isbns while updating");
|
|
|
|
for author_name in doc_sent.author_name.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_author::Model = book_author::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
author_name: author_name.to_owned(),
|
|
};
|
|
MutationCore::create_book_author(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
}
|
|
for person in doc_sent.person.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_person::Model = book_person::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
person: person.to_owned(),
|
|
};
|
|
MutationCore::create_book_person(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for place in doc_sent.place.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_place::Model = book_place::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
place: place.to_owned(),
|
|
};
|
|
MutationCore::create_book_place(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for subject in doc_sent.subject.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_subject::Model = book_subject::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
subject: subject.to_owned(),
|
|
};
|
|
MutationCore::create_book_subject(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for time in doc_sent.time.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_time::Model = book_time::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
time: time.to_owned(),
|
|
};
|
|
MutationCore::create_book_time(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
for isbn in doc_sent.isbn.as_ref().unwrap_or(&vec!["".to_string()]).iter() {
|
|
let record : book_isbn::Model = book_isbn::Model{
|
|
id: 1,
|
|
book_id: doc_sent.id,
|
|
isbn: isbn.to_owned(),
|
|
};
|
|
MutationCore::create_book_isbn(conn, record)
|
|
.await
|
|
.expect("could not create book");
|
|
|
|
}
|
|
|
|
let book_meili = BookMeili{
|
|
id: doc_sent.id,
|
|
open_library_key: doc_sent.open_library_key.unwrap_or("".to_string()),
|
|
title: doc_sent.title,
|
|
edition_count: doc_sent.edition_count.unwrap_or(0),
|
|
first_publish_year: doc_sent.first_publish_year.unwrap_or(0),
|
|
median_page_count: doc_sent.median_page_count.unwrap_or(0),
|
|
goodread_id: doc_sent.goodread_id.unwrap_or("".to_string()),
|
|
description: doc_sent.description.unwrap_or("".to_string()),
|
|
cover: cover.unwrap_or("".to_string()),
|
|
location: doc_sent.location.unwrap_or("".to_string()),
|
|
time_added: doc_sent.time_added.unwrap_or("".to_string()),
|
|
rating: doc_sent.rating.unwrap_or(0),
|
|
comments: doc_sent.comments.unwrap_or("".to_string()),
|
|
author_name: doc_sent.author_name.unwrap_or(vec!["".to_string()]),
|
|
person: doc_sent.person.unwrap_or(vec!["".to_string()]),
|
|
place: doc_sent.place.unwrap_or(vec!["".to_string()]),
|
|
subject: doc_sent.subject.unwrap_or(vec!["".to_string()]),
|
|
time: doc_sent.time.unwrap_or(vec!["".to_string()]),
|
|
isbn: doc_sent.isbn.unwrap_or(vec!["".to_string()]),
|
|
};
|
|
booksman_search::create_or_update_book(book_meili, userid, meili_client).await;
|
|
|
|
"success"
|
|
}
|