mod de;
mod error;
pub use crate::error::BmsTableError;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use htmlparser::{Token, Tokenizer};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::de::{deserialize_level, deserialize_level_order};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
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)]
#[non_exhaustive]
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(),
}
}
#[must_use]
pub fn level_index(&self, level: &str) -> Option<usize> {
self.level_order.iter().position(|l| l == level)
}
#[must_use]
pub fn effective_tag(&self) -> &str {
self.tag.as_deref().unwrap_or(&self.symbol)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct BmsTableData(pub Vec<ChartItem>);
impl BmsTableData {
#[must_use]
pub const fn new(charts: Vec<ChartItem>) -> Self {
Self(charts)
}
}
impl Deref for BmsTableData {
type Target = Vec<ChartItem>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BmsTableData {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for BmsTableData {
fn default() -> Self {
Self::new(Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum CourseGroup {
Courses(Vec<CourseInfo>),
SubGroups(Vec<CourseGroup>),
}
impl Default for CourseGroup {
fn default() -> Self {
Self::Courses(Vec::new())
}
}
impl CourseGroup {
#[must_use]
pub fn flatten(&self) -> Vec<&CourseInfo> {
match self {
Self::Courses(v) => v.iter().collect(),
Self::SubGroups(v) => v.iter().flat_map(CourseGroup::flatten).collect(),
}
}
#[must_use]
pub fn into_flattened(self) -> Vec<CourseInfo> {
match self {
Self::Courses(v) => v,
Self::SubGroups(v) => v
.into_iter()
.flat_map(CourseGroup::into_flattened)
.collect(),
}
}
}
impl From<CourseInfo> for CourseGroup {
fn from(info: CourseInfo) -> Self {
Self::Courses(vec![info])
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CourseInfo {
pub name: String,
#[serde(default)]
pub constraint: Vec<String>,
#[serde(default)]
pub trophy: Vec<Trophy>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub charts: Vec<ChartItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub md5: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sha256: Vec<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CourseInfo {
#[must_use]
pub const fn new(name: String) -> Self {
Self {
name,
constraint: Vec::new(),
trophy: Vec::new(),
charts: Vec::new(),
md5: Vec::new(),
sha256: Vec::new(),
extra: BTreeMap::new(),
}
}
#[must_use]
pub fn all_charts(&self) -> Vec<ChartItem> {
fn from_md5(hash: &str) -> ChartItem {
ChartItem {
md5: Some(hash.to_owned()),
level: "0".to_owned(),
..ChartItem::empty()
}
}
fn from_sha256(hash: &str) -> ChartItem {
ChartItem {
sha256: Some(hash.to_owned()),
level: "0".to_owned(),
..ChartItem::empty()
}
}
let mut result = self.charts.clone();
result.extend(self.md5.iter().map(|h| from_md5(h)));
result.extend(self.sha256.iter().map(|h| from_sha256(h)));
result
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChartItem {
#[serde(
default = "crate::de::default_level",
deserialize_with = "deserialize_level"
)]
pub level: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub md5: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub artist: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_diff: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl ChartItem {
#[must_use]
pub const fn empty() -> Self {
Self {
level: String::new(),
md5: None,
sha256: None,
title: None,
artist: None,
url: None,
url_diff: None,
comment: None,
extra: BTreeMap::new(),
}
}
#[must_use]
pub const fn new(level: String) -> Self {
Self {
level,
md5: None,
sha256: None,
title: None,
artist: None,
url: None,
url_diff: None,
comment: None,
extra: BTreeMap::new(),
}
}
}
impl Default for ChartItem {
fn default() -> Self {
Self::new(crate::de::default_level())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Trophy {
pub name: String,
pub missrate: f64,
pub scorerate: f64,
}
impl Trophy {
#[must_use]
pub const fn new(name: String, missrate: f64, scorerate: f64) -> Self {
Self {
name,
missrate,
scorerate,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BmsTableInfo {
pub name: String,
pub symbol: String,
pub url: url::Url,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl BmsTableInfo {
#[must_use]
pub const fn new(name: String, symbol: String, url: url::Url) -> Self {
Self {
name,
symbol,
url,
extra: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
#[non_exhaustive]
pub struct BmsTableList(pub Vec<BmsTableInfo>);
impl BmsTableList {
#[must_use]
pub const fn new(entries: Vec<BmsTableInfo>) -> Self {
Self(entries)
}
}
impl Deref for BmsTableList {
type Target = Vec<BmsTableInfo>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BmsTableList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub struct BmsTableHtml;
impl BmsTableHtml {
pub fn extract_url<'a>(html_content: &'a str) -> Result<&'a str, BmsTableError> {
let mut in_meta = false;
let mut is_bmstable = false;
let mut content: Option<&'a str> = None;
let mut tokenizer_error: Option<String> = None;
for token in Tokenizer::from(html_content) {
let token = match token {
Ok(t) => t,
Err(e) => {
if tokenizer_error.is_none() {
tokenizer_error = Some(e.to_string());
}
continue;
}
};
match token {
Token::ElementStart { local, .. } => {
in_meta = local.as_str().eq_ignore_ascii_case("meta");
if in_meta {
is_bmstable = false;
content = None;
}
}
Token::Attribute { local, value, .. } if in_meta => {
let name = local.as_str();
if name.eq_ignore_ascii_case("name") || name.eq_ignore_ascii_case("property") {
is_bmstable =
value.is_some_and(|v| v.as_str().eq_ignore_ascii_case("bmstable"));
} else if name.eq_ignore_ascii_case("content") {
content = value.map(|v| v.as_str());
}
}
Token::ElementEnd { .. } if in_meta => {
if is_bmstable
&& let Some(c) = content
&& !c.is_empty()
{
return Ok(c);
}
in_meta = false;
}
_ => {}
}
}
Err(tokenizer_error.map_or(
BmsTableError::MetaTagNotFound,
BmsTableError::TokenizerError,
))
}
}