#![warn(missing_docs)]
#![warn(clippy::must_use_candidate)]
#![deny(rustdoc::broken_intra_doc_links)]
mod de;
use std::collections::BTreeMap;
use anyhow::{Result, anyhow};
use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::de::{de_numstring, deserialize_level_order};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BmsTable {
pub header: BmsTableHeader,
pub data: BmsTableData,
}
impl BmsTable {
#[must_use]
pub const fn new(header: BmsTableHeader, data: BmsTableData) -> Self {
Self { header, data }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BmsTableHeader {
pub name: String,
pub symbol: String,
pub data_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(default)]
pub course: CourseGroup,
#[serde(default, deserialize_with = "deserialize_level_order")]
pub level_order: Vec<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl BmsTableHeader {
#[must_use]
pub const fn new(name: String, symbol: String, data_url: String) -> Self {
Self {
name,
symbol,
data_url,
tag: None,
mode: None,
course: CourseGroup::Courses(Vec::new()),
level_order: Vec::new(),
extra: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BmsTableData {
pub charts: Vec<ChartItem>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CourseGroup {
Courses(Vec<CourseInfo>),
SubGroups(Vec<CourseGroup>),
}
impl Default for CourseGroup {
fn default() -> Self {
Self::Courses(Vec::new())
}
}
impl CourseGroup {
pub fn flatten(&self) -> Vec<&CourseInfo> {
match self {
Self::Courses(v) => v.iter().collect(),
Self::SubGroups(v) => v.iter().flat_map(CourseGroup::flatten).collect(),
}
}
}
impl From<CourseInfo> for CourseGroup {
fn from(info: CourseInfo) -> Self {
Self::Courses(vec![info])
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "crate::de::CourseInfoRaw")]
pub struct CourseInfo {
pub name: String,
#[serde(default)]
pub constraint: Vec<String>,
#[serde(default)]
pub trophy: Vec<Trophy>,
#[serde(default)]
pub charts: Vec<ChartItem>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChartItem {
#[serde(default, deserialize_with = "de_numstring")]
pub level: String,
pub md5: Option<String>,
pub sha256: Option<String>,
pub title: Option<String>,
pub artist: Option<String>,
pub url: Option<String>,
pub url_diff: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl ChartItem {
#[must_use]
pub const fn new(level: String) -> Self {
Self {
level,
md5: None,
sha256: None,
title: None,
artist: None,
url: None,
url_diff: None,
extra: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Trophy {
pub name: String,
pub missrate: f64,
pub scorerate: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BmsTableInfo {
pub name: String,
pub symbol: String,
pub url: url::Url,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BmsTableList {
pub listes: Vec<BmsTableInfo>,
}
pub struct BmsTableHtml;
impl BmsTableHtml {
pub fn extract_url(html_content: &str) -> Result<String> {
let document = Html::parse_document(html_content);
let meta_selector = Selector::parse("meta").map_err(|_| anyhow!("meta tag not found"))?;
for element in document.select(&meta_selector) {
let is_bmstable = element
.value()
.attr("name")
.is_some_and(|v| v.eq_ignore_ascii_case("bmstable"))
|| element
.value()
.attr("property")
.is_some_and(|v| v.eq_ignore_ascii_case("bmstable"));
if is_bmstable
&& let Some(content_attr) = element.value().attr("content")
&& !content_attr.is_empty()
{
return Ok(content_attr.to_string());
}
}
Err(anyhow!("bmstable meta tag not found"))
}
}