use std::time::Duration;
use anyhow::{Context, Result};
use bms_table::{BmsTable, BmsTableData, BmsTableHeader, BmsTableHtml};
use reqwest::Client;
use url::Url;
#[tokio::main]
async fn main() -> Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://stellabms.xyz/sl/table.html".to_string());
let url = Url::parse(&url)?;
let client = build_client()?;
match fetch_table(&client, &url).await {
Ok(table) => {
println!(
"{}: {} charts, {} courses",
table.header.name,
table.data.len(),
table.header.course.flatten().len(),
);
}
Err(e) => {
eprintln!("Failed to fetch {url}");
eprintln!("Error: {e:#}");
}
}
Ok(())
}
async fn fetch_table(client: &Client, url: &Url) -> Result<BmsTable> {
let text = fetch_text(client, url, "table page").await?;
let header: BmsTableHeader;
let header_base_url: Url;
if let Ok(h) = parse_json_clean::<BmsTableHeader>(&text) {
header = h;
header_base_url = url.clone();
} else {
let meta_url = BmsTableHtml::extract_url(&text)
.context("Response is neither header JSON nor HTML with a bmstable meta tag")?;
let header_url = url.join(meta_url)?;
let header_text = fetch_text(client, &header_url, "header JSON").await?;
header = parse_json_clean(&header_text).context("Failed to parse header JSON")?;
header_base_url = header_url;
}
let data_url = header_base_url.join(&header.data_url)?;
let data_text = fetch_text(client, &data_url, "chart data JSON").await?;
let data: BmsTableData =
parse_json_clean(&data_text).context("Failed to parse chart data JSON")?;
Ok(BmsTable::new(header, data))
}
async fn fetch_text(client: &Client, url: &Url, label: &str) -> Result<String> {
client
.get(url.as_str())
.send()
.await
.with_context(|| format!("Failed to fetch {label}"))?
.text()
.await
.with_context(|| format!("Failed to read {label} body"))
}
fn parse_json_clean<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T> {
Ok(serde_json::from_str(raw).or_else(|_| {
let cleaned: String = raw.chars().filter(|c| !c.is_control()).collect();
serde_json::from_str(&cleaned)
})?)
}
fn build_client() -> Result<Client> {
Client::builder()
.user_agent("Mozilla/5.0 bms-table-rs example")
.timeout(Duration::from_secs(60))
.redirect(reqwest::redirect::Policy::limited(20))
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.context("Failed to build HTTP client")
}