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, make_lenient_client};
28//!
29//! let client = make_lenient_client()?;
30//! let table = fetch_table(&client, "https://stellabms.xyz/sl/table.html").await?;
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//! # 无网络使用(直接解析 JSON)
40//!
41//! ```rust,no_run
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#"{ "charts": [] }"#;
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//! # 获取难度表列表示例
58//!
59//! ```rust,no_run
60//! # #[tokio::main]
61//! # #[cfg(feature = "reqwest")]
62//! # async fn main() -> anyhow::Result<()> {
63//! use bms_table::fetch::reqwest::{fetch_table_list, make_lenient_client};
64//! let client = make_lenient_client()?;
65//! let indexes = fetch_table_list(&client, "https://example.com/table_index.json").await?;
66//! assert!(!indexes.is_empty());
67//! # Ok(())
68//! # }
69//! # #[cfg(not(feature = "reqwest"))]
70//! # fn main() {}
71//! ```
72//!
73//! 提示:启用 `reqwest` 特性将隐式启用 `scraper`,以支持从网页内容中定位 bmstable 头部地址。
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/// 顶层 BMS 难度表数据结构。
94///
95/// 将表头元数据与谱面数据打包在一起,便于在应用中一次性传递与使用。
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
98pub struct BmsTable {
99 /// 表头信息与额外字段
100 pub header: BmsTableHeader,
101 /// 表数据,包含谱面列表
102 pub data: BmsTableData,
103}
104
105/// BMS 表头信息。
106///
107/// 该结构严格解析常见字段,并把未识别的字段保存在 `extra` 中,保证向前兼容。
108#[derive(Debug, Clone, PartialEq)]
109#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
110pub struct BmsTableHeader {
111 /// 表格名称,如 "Satellite"
112 pub name: String,
113 /// 表格符号,如 "sl"
114 pub symbol: String,
115 /// 谱面数据文件的URL(原样保存来自header JSON的字符串)
116 pub data_url: String,
117 /// 课程信息数组,每个元素是一个课程组的数组
118 #[cfg_attr(
119 feature = "serde",
120 serde(default, deserialize_with = "deserialize_course_groups")
121 )]
122 pub course: Vec<Vec<CourseInfo>>,
123 /// 难度等级顺序,包含数字和字符串
124 #[cfg_attr(
125 feature = "serde",
126 serde(default, deserialize_with = "deserialize_level_order")
127 )]
128 pub level_order: Vec<String>,
129 /// 额外数据(来自header JSON中未识别的字段)
130 #[cfg(feature = "serde")]
131 #[cfg_attr(feature = "serde", serde(flatten))]
132 pub extra: BTreeMap<String, Value>,
133}
134
135/// BMS 表数据。
136///
137/// 仅包含谱面数组。解析时同时兼容纯数组与 `{ charts: [...] }` 两种输入形式。
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 /// 谱面数据
143 pub charts: Vec<ChartItem>,
144}
145
146/// 课程信息。
147///
148/// 描述一个课程的名称、约束、奖杯与谱面集合。解析阶段会将 `md5`/`sha256`
149/// 列表自动转换为对应的 `ChartItem`,并为缺失 `level` 的谱面补充默认值 `"0"`。
150#[derive(Debug, Clone, PartialEq)]
151#[cfg_attr(feature = "serde", derive(Serialize))]
152pub struct CourseInfo {
153 /// 课程名称,如 "Satellite Skill Analyzer 2nd sl0"
154 pub name: String,
155 /// 约束条件列表,如 ["grade_mirror", "gauge_lr2", "ln"]
156 #[cfg_attr(feature = "serde", serde(default))]
157 pub constraint: Vec<String>,
158 /// 奖杯信息列表,定义不同等级的奖杯要求
159 #[cfg_attr(feature = "serde", serde(default))]
160 pub trophy: Vec<Trophy>,
161 /// 谱面数据列表,包含该课程的所有谱面信息
162 #[cfg_attr(feature = "serde", serde(default))]
163 pub charts: Vec<ChartItem>,
164}
165
166/// 谱面数据项。
167///
168/// 描述单个 BMS 文件的相关元数据与资源链接。为空字符串的可选字段在反序列化时会
169/// 自动转换为 `None`,以提升数据质量。
170#[derive(Debug, Clone, PartialEq, Eq)]
171#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
172pub struct ChartItem {
173 /// 难度等级,如 "0"
174 #[cfg_attr(feature = "serde", serde(default, deserialize_with = "de_numstring"))]
175 pub level: String,
176 /// 文件的MD5哈希值
177 pub md5: Option<String>,
178 /// 文件的SHA256哈希值
179 pub sha256: Option<String>,
180 /// 歌曲标题
181 pub title: Option<String>,
182 /// 歌曲副标题
183 pub subtitle: Option<String>,
184 /// 艺术家名称
185 pub artist: Option<String>,
186 /// 歌曲副艺术家
187 pub subartist: Option<String>,
188 /// 文件下载链接
189 pub url: Option<String>,
190 /// 差分文件下载链接(可选)
191 pub url_diff: Option<String>,
192 /// 额外数据
193 #[cfg(feature = "serde")]
194 #[cfg_attr(feature = "serde", serde(flatten))]
195 pub extra: BTreeMap<String, Value>,
196}
197
198/// 奖杯信息。
199///
200/// 定义达成特定奖杯的条件,包括最大 miss 率与最低得分率等要求。
201#[derive(Debug, Clone, PartialEq)]
202#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
203pub struct Trophy {
204 /// 奖杯名称,如 "silvermedal" 或 "goldmedal"
205 pub name: String,
206 /// 最大miss率(百分比),如 5.0 表示最大5%的miss率
207 pub missrate: f64,
208 /// 最小得分率(百分比),如 70.0 表示至少70%的得分率
209 pub scorerate: f64,
210}
211
212/// 完整的原始 JSON 字符串集合。
213#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
214pub struct BmsTableRaw {
215 /// 头部 JSON 的完整地址
216 #[cfg(feature = "scraper")]
217 pub header_json_url: url::Url,
218 /// 原始表头 JSON 字符串
219 pub header_raw: String,
220 /// 谱面数据 JSON 的完整地址
221 #[cfg(feature = "scraper")]
222 pub data_json_url: url::Url,
223 /// 原始谱面数据 JSON 字符串
224 pub data_raw: String,
225}
226
227/// BMS 难度表列表条目。
228///
229/// 表示一个难度表在列表中的基本信息。仅要求 `name`、`symbol`、`url` 三个字段,
230/// 其余诸如 `tag1`、`tag2`、`comment`、`date`、`state`、`tag_order` 等字段统一收集到 `extra`。
231#[derive(Debug, Clone, PartialEq, Eq)]
232#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
233pub struct BmsTableInfo {
234 /// 表名称,如 ".WAS難易度表"
235 pub name: String,
236 /// 表符号,如 "." 或 "[F]"
237 pub symbol: String,
238 /// 表地址(为完整的 `url::Url` 类型)
239 #[cfg(feature = "scraper")]
240 pub url: url::Url,
241 /// 表地址(为完整的 `url::Url` 类型)
242 #[cfg(not(feature = "scraper"))]
243 pub url: String,
244 /// 额外字段集合(用于保存除必需字段外的所有数据)
245 #[cfg(feature = "serde")]
246 #[cfg_attr(feature = "serde", serde(flatten))]
247 pub extra: BTreeMap<String, Value>,
248}
249
250/// BMS 难度表列表包装类型。
251///
252/// 透明序列化为数组:序列化/反序列化时行为与内部的 `Vec<BmsTableInfo>` 相同,
253/// 因此序列化结果为一个 JSON 数组而不是对象。
254#[derive(Debug, Clone, PartialEq, Eq)]
255#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
256#[cfg_attr(feature = "serde", serde(transparent))]
257pub struct BmsTableList {
258 /// 列表条目数组
259 pub indexes: Vec<BmsTableInfo>,
260}