#![feature(lazy_cell)]
use actix_web::{get, http::header, web, App, Either, HttpResponse, HttpServer};
use data::{Card, CardInfo, Set};
use filter::SearchCard;
use itertools::Itertools;
use regex::{Captures, Regex};
use serde::Deserialize;
use std::{
collections::HashMap,
fmt::Write,
fs::File,
io::BufReader,
net::Ipv4Addr,
sync::{
atomic::{AtomicUsize, Ordering},
LazyLock,
},
time::Instant,
};
use time::Date;
mod data;
mod filter;
mod parser;
type AnyResult = Result>;
// The yearly tins have ~250 cards in them.
// I want to be higher than that so the page is usable as a set list.
const RESULT_LIMIT: usize = 300;
static CARDS: LazyLock> = LazyLock::new(|| {
let mut cards = serde_json::from_reader::<_, CardInfo>(BufReader::new(File::open("cards.json").expect("cards.json not found")))
.expect("Could not deserialize cards")
.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
});
static CARDS_BY_ID: LazyLock> = LazyLock::new(|| {
CARDS
.iter()
.map(|c| {
let text = PENDULUM_SEPARATOR
.replacen(&c.text.replace('\r', ""), 1, |caps: &Captures| {
format!("
[ {} ]", caps.iter().flatten().last().map_or_else(|| "Monster Effect", |g| g.as_str()))
})
.replace('\n', "
");
(c.id, Card { text, ..c.clone() })
})
.collect()
});
static SEARCH_CARDS: LazyLock> = LazyLock::new(|| CARDS.iter().map(SearchCard::from).collect());
static SETS_BY_NAME: LazyLock> = LazyLock::new(|| {
serde_json::from_reader::<_, Vec>(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()
});
static PENDULUM_SEPARATOR: LazyLock =
LazyLock::new(|| Regex::new("(\\n-+)?\\n\\[\\s?(Monster Effect|Flavor Text)\\s?\\]\\n?").unwrap());
static IMG_HOST: LazyLock = LazyLock::new(|| std::env::var("IMG_HOST").unwrap_or_else(|_| String::new()));
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let now = Instant::now();
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());
HttpServer::new(|| App::new().service(search).service(card_info).service(help))
.bind((Ipv4Addr::from([127, 0, 0, 1]), 1961))?
.run()
.await
}
#[derive(Debug, Deserialize)]
struct Query {
q: String,
}
#[derive(Debug)]
enum TargetPage {
Data(PageData),
Redirect(String),
}
#[derive(Debug)]
struct PageData {
description: String,
title: String,
query: Option,
body: String,
}
const HEADER: &str = include_str!("../static/header.html");
const HELP_CONTENT: &str = include_str!("../static/help.html");
static VIEW_COUNT: AtomicUsize = AtomicUsize::new(0);
fn footer() -> String {
format!(
r#"