aro/src/main.rs

177 lines
5.9 KiB
Rust
Raw Normal View History

2023-01-27 14:18:33 +01:00
#![feature(option_result_contains, once_cell)]
use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
2023-02-02 11:34:58 +01:00
use data::{Card, CardInfo, Set};
2023-01-26 23:07:16 +01:00
use filter::SearchCard;
2023-01-27 15:48:07 +01:00
use itertools::Itertools;
2023-01-27 14:18:33 +01:00
use serde::Deserialize;
use std::{collections::HashMap, fmt::Write, fs::File, io::BufReader, net::Ipv4Addr, sync::LazyLock, time::Instant};
2023-02-02 11:34:58 +01:00
use time::Date;
2023-01-26 23:07:16 +01:00
mod data;
mod filter;
mod parser;
2023-01-27 14:45:17 +01:00
const RESULT_LIMIT: usize = 100;
2023-01-27 14:18:33 +01:00
static CARDS: LazyLock<Vec<Card>> = LazyLock::new(|| {
2023-02-02 11:34:58 +01:00
let mut cards = serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found")))
2023-01-27 14:18:33 +01:00
.expect("Could not deserialize cards")
2023-02-02 11:34:58 +01:00
.data;
cards.iter_mut().for_each(|c| {
c.card_sets.sort_unstable_by_key(|s| SETS_BY_NAME.get(&s.set_name.to_lowercase()).and_then(|s| s.tcg_date).unwrap_or(Date::MAX))
});
cards
2023-01-27 14:18:33 +01:00
});
static CARDS_BY_ID: LazyLock<HashMap<usize, Card>> =
2023-01-31 11:47:59 +01:00
LazyLock::new(|| CARDS.iter().map(|c| (c.id, Card { text: c.text.replace('\r', "").replace('\n', "<br/>"), ..c.clone() })).collect());
2023-01-27 14:18:33 +01:00
static SEARCH_CARDS: LazyLock<Vec<SearchCard>> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect());
2023-02-02 11:34:58 +01:00
static SETS_BY_NAME: LazyLock<HashMap<String, Set>> = LazyLock::new(|| {
serde_json::from_reader::<_, Vec<Set>>(BufReader::new(File::open("sets.json").expect("sets.json not found")))
.expect("Could not deserialize sets")
.into_iter()
.map(|s| (s.set_name.to_lowercase(), s))
.collect()
});
2023-01-27 14:18:33 +01:00
2023-01-31 17:08:37 +01:00
static IMG_HOST: LazyLock<String> = LazyLock::new(|| std::env::var("IMG_HOST").unwrap_or_else(|_| String::new()));
2023-01-27 14:18:33 +01:00
#[actix_web::main]
async fn main() -> std::io::Result<()> {
2023-01-27 00:03:00 +01:00
let now = Instant::now();
2023-01-27 14:18:33 +01:00
println!("Starting server");
// tap these so they’re initialized
let num_cards = (CARDS_BY_ID.len() + SEARCH_CARDS.len()) / 2;
println!("Read {num_cards} cards in {:?}", now.elapsed());
2023-01-30 17:27:44 +01:00
HttpServer::new(|| App::new().service(search).service(card_info).service(help))
2023-01-30 18:00:46 +01:00
.bind((Ipv4Addr::from([127, 0, 0, 1]), 1961))?
2023-01-30 17:27:44 +01:00
.run()
.await
2023-01-27 14:18:33 +01:00
}
#[derive(Debug, Deserialize)]
struct Query {
q: String,
}
2023-01-30 11:39:42 +01:00
const HEADER: &str = include_str!("../static/header.html");
2023-01-30 17:27:44 +01:00
const HELP_CONTENT: &str = include_str!("../static/help.html");
const FOOTER: &str = r#"<div id="bottom">
<a href="/">Home</a>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="/help">Query Syntax</a>
</div>
</body></html>"#;
2023-01-30 11:39:42 +01:00
2023-01-27 14:18:33 +01:00
#[get("/")]
async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let q = match q {
Some(Either::Left(web::Query(Query { q }))) => Some(q),
Some(Either::Right(web::Form(Query { q }))) => Some(q),
None => None,
2023-01-30 15:57:08 +01:00
}
.filter(|s| !s.is_empty());
2023-01-27 14:18:33 +01:00
let mut res = String::with_capacity(10_000);
2023-01-30 15:57:08 +01:00
res.push_str(HEADER);
render_searchbox(&mut res, &q)?;
match q {
Some(q) => render_results(&mut res, &q)?,
None => res.push_str("Enter a query above to search"),
}
finish_document(&mut res);
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
}
2023-01-30 17:27:44 +01:00
#[get("/card/{id}")]
2023-01-30 15:57:08 +01:00
async fn card_info(card_id: web::Path<usize>) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let mut res = String::with_capacity(2_000);
res.push_str(HEADER);
render_searchbox(&mut res, &None)?;
match CARDS_BY_ID.get(&card_id) {
Some(card) => {
res.push_str(r#""#);
write!(
res,
r#"
2023-01-31 17:08:37 +01:00
<div>
<img class="fullimage" src="{}/static/full/{}.jpg"/>
{card}
{}
2023-01-30 15:57:08 +01:00
</div>"#,
2023-01-31 17:08:37 +01:00
IMG_HOST.as_str(),
2023-01-30 15:57:08 +01:00
card.id,
card.extended_info().unwrap_or_else(|_| String::new()),
2023-01-30 15:57:08 +01:00
)?;
}
None => res.push_str("Card not found"),
}
2023-01-30 18:19:52 +01:00
finish_document(&mut res);
2023-01-30 15:57:08 +01:00
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
}
2023-01-30 17:27:44 +01:00
#[get("/help")]
async fn help() -> Result<HttpResponse, Box<dyn std::error::Error>> {
let mut res = String::with_capacity(HEADER.len() + HELP_CONTENT.len() + FOOTER.len() + 250);
res.push_str(HEADER);
render_searchbox(&mut res, &None)?;
res.push_str(HELP_CONTENT);
res.push_str(FOOTER);
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
}
2023-01-30 15:57:08 +01:00
fn render_searchbox(res: &mut String, query: &Option<String>) -> std::fmt::Result {
2023-01-27 14:18:33 +01:00
write!(
res,
r#"
<form action="/">
2023-01-30 11:39:42 +01:00
<input type="text" name="q" id="searchbox" placeholder="Enter query (e.g. l:5 c:synchro atk>2000)" value="{}"><input type="submit" id="submit" value="🔍">
2023-01-27 14:18:33 +01:00
</form>"#,
2023-01-30 15:57:08 +01:00
match &query {
2023-01-31 11:47:59 +01:00
Some(q) => q.replace('"', "&quot;"),
None => String::new(),
2023-01-27 14:18:33 +01:00
}
2023-01-30 15:57:08 +01:00
)
}
fn render_results(res: &mut String, query: &str) -> Result<(), Box<dyn std::error::Error>> {
2023-01-31 11:47:59 +01:00
let (raw_filters, query) = match parser::parse_filters(query) {
2023-01-30 15:57:08 +01:00
Ok(q) => q,
Err(e) => {
write!(res, "Could not parse query: {e:?}")?;
return Ok(());
}
};
let now = Instant::now();
let matches: Vec<&Card> = SEARCH_CARDS
.iter()
2023-01-31 11:47:59 +01:00
.filter(|card| query.iter().all(|q| q(card)))
2023-01-30 15:57:08 +01:00
.map(|c| CARDS_BY_ID.get(&c.id).unwrap())
.take(RESULT_LIMIT)
.collect();
write!(
res,
"<span class=\"meta\">Showing {} results where {} (took {:?})</span>",
matches.len(),
2023-01-31 11:47:59 +01:00
raw_filters.iter().map(|f| f.to_string()).join(" and "),
2023-01-30 15:57:08 +01:00
now.elapsed()
2023-01-27 14:18:33 +01:00
)?;
2023-01-30 15:57:08 +01:00
if matches.is_empty() {
return Ok(());
}
2023-01-31 17:08:37 +01:00
res.push_str("<div style=\"display: flex; flex-wrap: wrap;\">");
2023-01-30 15:57:08 +01:00
for card in matches {
2023-01-27 15:48:07 +01:00
write!(
res,
r#"<a class="cardresult" href="/card/{}"><img src="{}/static/thumb/{}.jpg" class="thumb"/>{card}</a>"#,
2023-01-31 17:08:37 +01:00
card.id,
IMG_HOST.as_str(),
card.id
2023-01-27 15:48:07 +01:00
)?;
}
2023-01-31 17:08:37 +01:00
res.push_str("</div>");
2023-01-30 15:57:08 +01:00
Ok(())
}
fn finish_document(res: &mut String) {
2023-01-30 17:27:44 +01:00
res.push_str(FOOTER)
2022-10-05 17:57:01 +02:00
}