bms_table/
lib.rs

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