bms_table/
lib.rs

1//! BMS 难度表获取与解析
2//!
3//! 提供从网页或头部 JSON 构建完整的 BMS 难度表数据结构,涵盖表头、课程、奖杯与谱面条目。
4//! 结合特性开关实现网络抓取与 HTML 解析,适用于 CLI 工具、服务端程序或数据处理流水线。
5//!
6//! # 功能一览
7//!
8//! - 解析表头 JSON,未识别字段保留在 `extra` 以保证向前兼容;
9//! - 解析谱面数据,兼容纯数组与 `{ charts: [...] }` 两种格式;
10//! - 课程支持将 `md5`/`sha256` 列表自动转换为谱面条目,缺失 `level` 时补为 "0";
11//! - 从 HTML 的 `<meta name="bmstable">` 提取头部 JSON 地址;
12//! - 一站式网络获取 API(网页 → 头部 JSON → 谱面数据);
13//! - 支持获取难度表索引列表。
14//!
15//! # 特性开关
16//!
17//! - `serde`:启用类型的序列化/反序列化支持(默认启用)。
18//! - `scraper`:启用 HTML 解析与 bmstable 头部地址提取(默认启用;`reqwest` 隐式启用该特性)。
19//! - `reqwest`:启用网络获取实现(默认启用;需要 `tokio` 运行时)。
20//!
21//! # 快速上手(网络获取)
22//!
23//! ```rust,no_run
24//! # #[tokio::main]
25//! # #[cfg(feature = "reqwest")]
26//! # async fn main() -> anyhow::Result<()> {
27//! use bms_table::fetch::reqwest::fetch_table;
28//!
29//! let table = fetch_table("https://stellabms.xyz/sl/table.html").await?;
30//! println!("{}: {} charts", table.header.name, table.data.charts.len());
31//! # Ok(())
32//! # }
33//! #
34//! # #[cfg(not(feature = "reqwest"))]
35//! # fn main() {}
36//! ```
37//!
38//! # 无网络使用(直接解析 JSON)
39//!
40//! ```rust,no_run
41//! # #[cfg(feature = "serde")]
42//! # fn main() -> anyhow::Result<()> {
43//! use bms_table::{BmsTable, BmsTableHeader, BmsTableData};
44//!
45//! let header_json = r#"{ "name": "Test", "symbol": "t", "data_url": "charts.json", "course": [], "level_order": [] }"#;
46//! let data_json = r#"{ "charts": [] }"#;
47//! let header: BmsTableHeader = serde_json::from_str(header_json)?;
48//! let data: BmsTableData = serde_json::from_str(data_json)?;
49//! let _table = BmsTable { header, data };
50//! # Ok(())
51//! # }
52//! # #[cfg(not(feature = "serde"))]
53//! # fn main() {}
54//! ```
55//!
56//! # 获取索引列表示例
57//!
58//! ```rust,no_run
59//! # #[tokio::main]
60//! # #[cfg(feature = "reqwest")]
61//! # async fn main() -> anyhow::Result<()> {
62//! use bms_table::fetch::reqwest::fetch_table_index;
63//! let indexes = fetch_table_index("https://example.com/table_index.json").await?;
64//! assert!(!indexes.is_empty());
65//! # Ok(())
66//! # }
67//! # #[cfg(not(feature = "reqwest"))]
68//! # fn main() {}
69//! ```
70//!
71//! 提示:启用 `reqwest` 特性将隐式启用 `scraper`,以支持从网页内容中定位 bmstable 头部地址。
72
73#![warn(missing_docs)]
74#![warn(clippy::must_use_candidate)]
75#![deny(rustdoc::broken_intra_doc_links)]
76#![cfg_attr(docsrs, feature(doc_cfg))]
77
78pub mod de;
79pub mod fetch;
80
81#[cfg(feature = "serde")]
82use serde::{Deserialize, Serialize};
83#[cfg(feature = "serde")]
84use serde_json::Value;
85use std::collections::HashMap;
86
87/// 顶层 BMS 难度表数据结构。
88///
89/// 将表头元数据与谱面数据打包在一起,便于在应用中一次性传递与使用。
90#[derive(Debug, Clone, PartialEq)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92pub struct BmsTable {
93    /// 表头信息与额外字段
94    pub header: BmsTableHeader,
95    /// 表数据,包含谱面列表
96    pub data: BmsTableData,
97}
98
99/// BMS 表头信息。
100///
101/// 该结构严格解析常见字段,并把未识别的字段保存在 `extra` 中,保证向前兼容。
102#[derive(Debug, Clone, PartialEq)]
103#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
104pub struct BmsTableHeader {
105    /// 表格名称,如 "Satellite"
106    pub name: String,
107    /// 表格符号,如 "sl"
108    pub symbol: String,
109    /// 谱面数据文件的URL(原样保存来自header JSON的字符串)
110    pub data_url: String,
111    /// 课程信息数组,每个元素是一个课程组的数组
112    #[cfg_attr(
113        feature = "serde",
114        serde(default, deserialize_with = "crate::de::deserialize_course_groups")
115    )]
116    pub course: Vec<Vec<CourseInfo>>,
117    /// 难度等级顺序,包含数字和字符串
118    #[cfg_attr(
119        feature = "serde",
120        serde(default, deserialize_with = "crate::de::deserialize_level_order")
121    )]
122    pub level_order: Vec<String>,
123    /// 额外数据(来自header JSON中未识别的字段)
124    #[cfg(feature = "serde")]
125    #[cfg_attr(feature = "serde", serde(flatten))]
126    pub extra: HashMap<String, Value>,
127}
128
129/// BMS 表数据。
130///
131/// 仅包含谱面数组。解析时同时兼容纯数组与 `{ charts: [...] }` 两种输入形式。
132#[derive(Debug, Clone, PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
134#[cfg_attr(feature = "serde", serde(transparent))]
135pub struct BmsTableData {
136    /// 谱面数据
137    pub charts: Vec<ChartItem>,
138}
139
140/// 课程信息。
141///
142/// 描述一个课程的名称、约束、奖杯与谱面集合。解析阶段会将 `md5`/`sha256`
143/// 列表自动转换为对应的 `ChartItem`,并为缺失 `level` 的谱面补充默认值 `"0"`。
144#[derive(Debug, Clone, PartialEq)]
145#[cfg_attr(feature = "serde", derive(Serialize))]
146pub struct CourseInfo {
147    /// 课程名称,如 "Satellite Skill Analyzer 2nd sl0"
148    pub name: String,
149    /// 约束条件列表,如 ["grade_mirror", "gauge_lr2", "ln"]
150    #[cfg_attr(feature = "serde", serde(default))]
151    pub constraint: Vec<String>,
152    /// 奖杯信息列表,定义不同等级的奖杯要求
153    #[cfg_attr(feature = "serde", serde(default))]
154    pub trophy: Vec<Trophy>,
155    /// 谱面数据列表,包含该课程的所有谱面信息
156    #[cfg_attr(feature = "serde", serde(default))]
157    pub charts: Vec<ChartItem>,
158}
159
160/// 谱面数据项。
161///
162/// 描述单个 BMS 文件的相关元数据与资源链接。为空字符串的可选字段在反序列化时会
163/// 自动转换为 `None`,以提升数据质量。
164#[derive(Debug, Clone, PartialEq, Eq)]
165#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
166pub struct ChartItem {
167    /// 难度等级,如 "0"
168    pub level: String,
169    /// 文件的MD5哈希值
170    #[cfg_attr(
171        feature = "serde",
172        serde(default, deserialize_with = "crate::de::empty_string_as_none")
173    )]
174    pub md5: Option<String>,
175    /// 文件的SHA256哈希值
176    #[cfg_attr(
177        feature = "serde",
178        serde(default, deserialize_with = "crate::de::empty_string_as_none")
179    )]
180    pub sha256: Option<String>,
181    /// 歌曲标题
182    #[cfg_attr(
183        feature = "serde",
184        serde(default, deserialize_with = "crate::de::empty_string_as_none")
185    )]
186    pub title: Option<String>,
187    /// 歌曲副标题
188    #[cfg_attr(
189        feature = "serde",
190        serde(default, deserialize_with = "crate::de::empty_string_as_none")
191    )]
192    pub subtitle: Option<String>,
193    /// 艺术家名称
194    #[cfg_attr(
195        feature = "serde",
196        serde(default, deserialize_with = "crate::de::empty_string_as_none")
197    )]
198    pub artist: Option<String>,
199    /// 歌曲副艺术家
200    #[cfg_attr(
201        feature = "serde",
202        serde(default, deserialize_with = "crate::de::empty_string_as_none")
203    )]
204    pub subartist: Option<String>,
205    /// 文件下载链接
206    #[cfg_attr(
207        feature = "serde",
208        serde(default, deserialize_with = "crate::de::empty_string_as_none")
209    )]
210    pub url: Option<String>,
211    /// 差分文件下载链接(可选)
212    #[cfg_attr(
213        feature = "serde",
214        serde(default, deserialize_with = "crate::de::empty_string_as_none")
215    )]
216    pub url_diff: Option<String>,
217    /// 额外数据
218    #[cfg(feature = "serde")]
219    #[cfg_attr(feature = "serde", serde(flatten))]
220    pub extra: HashMap<String, Value>,
221}
222
223/// 奖杯信息。
224///
225/// 定义达成特定奖杯的条件,包括最大 miss 率与最低得分率等要求。
226#[derive(Debug, Clone, PartialEq)]
227#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
228pub struct Trophy {
229    /// 奖杯名称,如 "silvermedal" 或 "goldmedal"
230    pub name: String,
231    /// 最大miss率(百分比),如 5.0 表示最大5%的miss率
232    pub missrate: f64,
233    /// 最小得分率(百分比),如 70.0 表示至少70%的得分率
234    pub scorerate: f64,
235}
236
237/// 完整的原始 JSON 字符串集合。
238#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
239pub struct BmsTableRaw {
240    /// 原始表头 JSON 字符串
241    pub header_raw: String,
242    /// 原始谱面数据 JSON 字符串
243    pub data_raw: String,
244}
245
246/// BMS 表索引条目。
247///
248/// 表示一个难度表在索引列表中的基本信息。仅要求 `name`、`symbol`、`url` 三个字段,
249/// 其余诸如 `tag1`、`tag2`、`comment`、`date`、`state`、`tag_order` 等字段统一收集到 `extra`。
250#[derive(Debug, Clone, PartialEq, Eq)]
251#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
252pub struct BmsTableIndexItem {
253    /// 表名称,如 ".WAS難易度表"
254    pub name: String,
255    /// 表符号,如 "." 或 "[F]"
256    pub symbol: String,
257    /// 表地址(为完整的 `url::Url` 类型)
258    pub url: url::Url,
259    /// 额外字段集合(用于保存除必需字段外的所有数据)
260    #[cfg(feature = "serde")]
261    #[cfg_attr(feature = "serde", serde(flatten))]
262    pub extra: HashMap<String, Value>,
263}
264
265/// BMS 表索引列表包装类型。
266///
267/// 透明序列化为数组:序列化/反序列化时行为与内部的 `Vec<BmsTableIndexItem>` 相同,
268/// 因此序列化结果为一个 JSON 数组而不是对象。
269#[derive(Debug, Clone, PartialEq, Eq)]
270#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
271#[cfg_attr(feature = "serde", serde(transparent))]
272pub struct BmsTableIndex {
273    /// 索引条目数组
274    pub indexes: Vec<BmsTableIndexItem>,
275}