use std::time::Duration;
use anyhow::{Context, Result};
use bms_table::{BmsTable, BmsTableData, BmsTableHeader, BmsTableHtml};
use reqwest::Client;
use tokio::sync::mpsc;
use url::Url;
#[tokio::main]
async fn main() -> Result<()> {
let client = build_client()?;
let urls = get_urls();
println!("Fetching {} tables concurrently...\n", urls.len());
let (tx, mut rx) = mpsc::channel::<FetchResult>(32);
let handler = tokio::spawn(async move {
while let Some(result) = rx.recv().await {
match result.table {
Ok(table) => {
println!(
"[OK] {} ({} charts, {} courses)",
table.header.name,
table.data.len(),
table.header.course.flatten().len(),
);
}
Err(e) => {
println!("[ERR] {}: {e:#}", result.label);
}
}
}
});
for url in urls {
let tx = tx.clone();
let client = client.clone();
tokio::spawn(async move {
let result = fetch_one(&client, &url).await;
let _ = tx.send(result).await;
});
}
drop(tx);
handler.await?;
println!("\nDone.");
Ok(())
}
struct FetchResult {
label: String,
table: Result<BmsTable>,
}
async fn fetch_one(client: &Client, url: &Url) -> FetchResult {
match fetch_table(client, url).await {
Ok(t) => FetchResult {
label: t.header.name.clone(),
table: Ok(t),
},
Err(e) => FetchResult {
label: url.to_string(),
table: Err(e),
},
}
}
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")
}
fn get_urls() -> Vec<Url> {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
args.iter()
.skip(1)
.filter_map(|s| Url::parse(s).ok())
.collect()
} else {
vec![
"https://stellabms.xyz/sl/table.html",
"https://stellabms.xyz/dp/table.html",
"https://zris.work/bmstable/normal/normal_header.json",
"https://zris.work/bmstable/insane/insane_header.json",
"http://rattoto10.jounin.jp/table_overjoy.html",
"http://rattoto10.jounin.jp/table.html",
"http://rattoto10.jounin.jp/table_insane.html",
"http://walkure.net/hakkyou/for_glassist/bms/?lamp=easy",
"http://walkure.net/hakkyou/for_glassist/bms/?lamp=normal",
"http://walkure.net/hakkyou/for_glassist/bms/?lamp=hard",
"http://walkure.net/hakkyou/for_glassist/bms/?lamp=fc",
"https://notmichaelchen.github.io/stella-table-extensions/stellalite.html",
"https://iidxtool.kasacontent.com/homage/table.php",
"https://zris.work/bmstable/dp_normal/dpn_header.json",
"https://pmsdifficulty.xxxxxxxx.jp/PMSdifficulty.html",
"https://pmsdifficulty.xxxxxxxx.jp/insane_PMSdifficulty.html",
"http://zris.work/bmstable/pms_insane/insane_pmsdatabase_header.json",
"https://pmsdifficulty.xxxxxxxx.jp/_pastoral_insane_table.html",
"https://www.notepara.com/glassist/10k",
"http://onzonium.at-ninja.jp/dp/",
"https://lr2.sakura.ne.jp/overjoy.php",
]
.into_iter()
.filter_map(|s| Url::parse(s).ok())
.collect()
}
}