bms_table/
lib.rs

1//! BMS difficulty table fetching and parsing
2//!
3//! Provides building a complete BMS difficulty table data structure from a web page or a header JSON, covering the header, courses, trophies, and chart items.
4//! Combined with feature flags, it implements network fetching and HTML parsing, suitable for CLI tools, server programs, or data-processing pipelines.
5//!
6//! # Feature overview
7//!
8//! - Parse header JSON into [`BmsTableHeader`], preserving unrecognized fields in `extra` for forward compatibility;
9//! - Parse chart data into [`BmsTableData`], supporting a plain array of [`ChartItem`] structure;
10//! - Courses automatically convert `md5`/`sha256` lists into chart items, filling missing `level` with "0";
11//! - Extract the header JSON URL from HTML `<meta name="bmstable">`;
12//! - One-stop network fetching APIs (web page → header JSON → chart data);
13//! - Support fetching a list of difficulty tables into [`BmsTableList`]. [An example source page](https://darksabun.club/table/tablelist.html).
14//!
15//! # Feature flags
16//!
17//! - `serde`: enable serialization/deserialization support for types (enabled by default).
18//! - `scraper`: enable HTML parsing and bmstable header URL extraction (enabled by default; implicitly enabled by `reqwest`).
19//! - `reqwest`: enable the network fetching implementation (enabled by default; requires the `tokio` runtime).
20//!
21//! # Quick start (network fetching)
22//!
23//! ```rust,no_run
24//! # #[tokio::main]
25//! # #[cfg(feature = "reqwest")]
26//! # async fn main() -> anyhow::Result<()> {
27//! use bms_table::fetch::reqwest::Fetcher;
28//!
29//! let fetcher = Fetcher::lenient()?;
30//! let table = fetcher.fetch_table("https://stellabms.xyz/sl/table.html").await?.table;
31//! println!("{}: {} charts", table.header.name, table.data.charts.len());
32//! # Ok(())
33//! # }
34//! #
35//! # #[cfg(not(feature = "reqwest"))]
36//! # fn main() {}
37//! ```
38//!
39//! # Offline usage (parse JSON directly)
40//!
41//! ```rust
42//! # #[cfg(feature = "serde")]
43//! # fn main() -> anyhow::Result<()> {
44//! use bms_table::{BmsTable, BmsTableHeader, BmsTableData};
45//!
46//! let header_json = r#"{ "name": "Test", "symbol": "t", "data_url": "charts.json", "course": [], "level_order": [] }"#;
47//! let data_json = r#"[]"#;
48//! let header: BmsTableHeader = serde_json::from_str(header_json)?;
49//! let data: BmsTableData = serde_json::from_str(data_json)?;
50//! let _table = BmsTable { header, data };
51//! # Ok(())
52//! # }
53//! # #[cfg(not(feature = "serde"))]
54//! # fn main() {}
55//! ```
56//!
57//! # Example: fetch table list
58//!
59//! ```rust,no_run
60//! # #[tokio::main]
61//! # #[cfg(feature = "reqwest")]
62//! # async fn main() -> anyhow::Result<()> {
63//! use bms_table::fetch::reqwest::Fetcher;
64//! let fetcher = Fetcher::lenient()?;
65//! let listes = fetcher.fetch_table_list("https://example.com/table_list.json").await?.tables;
66//! assert!(!listes.is_empty());
67//! # Ok(())
68//! # }
69//! # #[cfg(not(feature = "reqwest"))]
70//! # fn main() {}
71//! ```
72//!
73//! Hint: enabling the `reqwest` feature implicitly enables `scraper` to support locating the bmstable header URL from page content.
74
75#![warn(missing_docs)]
76#![warn(clippy::must_use_candidate)]
77#![deny(rustdoc::broken_intra_doc_links)]
78#![cfg_attr(docsrs, feature(doc_cfg))]
79
80pub mod de;
81pub mod fetch;
82
83#[cfg(feature = "serde")]
84use serde::{Deserialize, Serialize};
85#[cfg(feature = "serde")]
86use serde_json::Value;
87#[cfg(feature = "serde")]
88use std::collections::BTreeMap;
89
90#[cfg(feature = "serde")]
91use crate::de::{de_numstring, deserialize_course_groups, deserialize_level_order};
92
93/// Top-level BMS difficulty table data structure.
94///
95/// Packs header metadata and chart data together to simplify passing and use in applications.
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
98pub struct BmsTable {
99    /// Header information and extra fields
100    pub header: BmsTableHeader,
101    /// Table data containing the chart list
102    pub data: BmsTableData,
103}
104
105/// BMS header information.
106///
107/// Strictly parses common fields and preserves unrecognized fields in `extra` for forward compatibility.
108#[derive(Debug, Clone, PartialEq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub struct BmsTableHeader {
111    /// Table name, e.g. "Satellite"
112    pub name: String,
113    /// Table symbol, e.g. "sl"
114    pub symbol: String,
115    /// URL of chart data file (preserves the original string from header JSON)
116    pub data_url: String,
117    /// Course information as an array of course groups
118    #[cfg_attr(
119        feature = "serde",
120        serde(default, deserialize_with = "deserialize_course_groups")
121    )]
122    pub course: Vec<Vec<CourseInfo>>,
123    /// Difficulty level order containing numbers and strings
124    #[cfg_attr(
125        feature = "serde",
126        serde(default, deserialize_with = "deserialize_level_order")
127    )]
128    pub level_order: Vec<String>,
129    /// Extra data (unrecognized fields from header JSON)
130    #[cfg(feature = "serde")]
131    #[cfg_attr(feature = "serde", serde(flatten))]
132    pub extra: BTreeMap<String, Value>,
133}
134
135/// BMS table data.
136///
137/// Contains only the chart array. Parsing supports both a plain array and `{ charts: [...] }` input forms.
138#[derive(Debug, Clone, PartialEq, Eq)]
139#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
140#[cfg_attr(feature = "serde", serde(transparent))]
141pub struct BmsTableData {
142    /// Charts
143    pub charts: Vec<ChartItem>,
144}
145
146/// Course information.
147///
148/// Describes a course's name, constraints, trophies and chart set. During parsing, `md5`/`sha256` lists are automatically converted into `ChartItem`s, and charts missing `level` are filled with default value `"0"`.
149#[derive(Debug, Clone, PartialEq)]
150#[cfg_attr(feature = "serde", derive(Serialize))]
151pub struct CourseInfo {
152    /// Course name, e.g. "Satellite Skill Analyzer 2nd sl0"
153    pub name: String,
154    /// Constraint list, e.g. ["`grade_mirror`", "`gauge_lr2`", "ln"]
155    #[cfg_attr(feature = "serde", serde(default))]
156    pub constraint: Vec<String>,
157    /// List of trophies, defining requirements for different ranks
158    #[cfg_attr(feature = "serde", serde(default))]
159    pub trophy: Vec<Trophy>,
160    /// List of charts included in the course
161    #[cfg_attr(feature = "serde", serde(default))]
162    pub charts: Vec<ChartItem>,
163}
164
165/// Chart data item.
166///
167/// Describes metadata and resource links for a single BMS file.
168#[derive(Debug, Clone, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
170pub struct ChartItem {
171    /// Difficulty level, e.g. "0"
172    #[cfg_attr(feature = "serde", serde(default, deserialize_with = "de_numstring"))]
173    pub level: String,
174    /// MD5 hash of the file
175    pub md5: Option<String>,
176    /// SHA256 hash of the file
177    pub sha256: Option<String>,
178    /// Song title
179    pub title: Option<String>,
180    /// Song subtitle
181    pub subtitle: Option<String>,
182    /// Artist name
183    pub artist: Option<String>,
184    /// Song sub-artist
185    pub subartist: Option<String>,
186    /// File download URL
187    pub url: Option<String>,
188    /// Differential file download URL (optional)
189    pub url_diff: Option<String>,
190    /// Extra data
191    #[cfg(feature = "serde")]
192    #[cfg_attr(feature = "serde", serde(flatten))]
193    pub extra: BTreeMap<String, Value>,
194}
195
196/// Trophy information.
197///
198/// Defines conditions to achieve specific trophies, including maximum miss rate and minimum score rate.
199#[derive(Debug, Clone, PartialEq)]
200#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
201pub struct Trophy {
202    /// Trophy name, e.g. "silvermedal" or "goldmedal"
203    pub name: String,
204    /// Maximum miss rate (percent), e.g. 5.0 means at most 5% miss rate
205    pub missrate: f64,
206    /// Minimum score rate (percent), e.g. 70.0 means at least 70% score rate
207    pub scorerate: f64,
208}
209
210/// Complete set of original JSON strings.
211#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
212pub struct BmsTableRaw {
213    /// Full URL of the header JSON
214    #[cfg(feature = "scraper")]
215    pub header_json_url: url::Url,
216    /// Raw header JSON string
217    pub header_raw: String,
218    /// Full URL of the chart data JSON
219    #[cfg(feature = "scraper")]
220    pub data_json_url: url::Url,
221    /// Raw chart data JSON string
222    pub data_raw: String,
223}
224
225/// BMS difficulty table list item.
226///
227/// Represents the basic information of a difficulty table in a list. Only `name`, `symbol`, and `url` are required; other fields such as `tag1`, `tag2`, `comment`, `date`, `state`, and `tag_order` are collected into `extra`.
228#[derive(Debug, Clone, PartialEq, Eq)]
229#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230pub struct BmsTableInfo {
231    /// Table name, e.g. ".WAS Difficulty Table"
232    pub name: String,
233    /// Table symbol, e.g. "." or "[F]"
234    pub symbol: String,
235    /// Table URL (as a full `url::Url` type)
236    #[cfg(feature = "scraper")]
237    pub url: url::Url,
238    /// Table URL (as a full `url::Url` type)
239    #[cfg(not(feature = "scraper"))]
240    pub url: String,
241    /// Extra fields collection (stores all data except required fields)
242    #[cfg(feature = "serde")]
243    #[cfg_attr(feature = "serde", serde(flatten))]
244    pub extra: BTreeMap<String, Value>,
245}
246
247/// Wrapper type for the list of BMS difficulty tables.
248///
249/// Transparently serialized as an array: serialization/deserialization behaves the same as the internal `Vec<BmsTableInfo>`, resulting in a JSON array rather than an object.
250#[derive(Debug, Clone, PartialEq, Eq)]
251#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
252#[cfg_attr(feature = "serde", serde(transparent))]
253pub struct BmsTableList {
254    /// List of entries
255    pub listes: Vec<BmsTableInfo>,
256}