bms_table/fetch/
reqwest.rs

1//! 基于 `reqwest` 的网络获取模块
2//!
3//! 提供一站式从网页或头部 JSON 源拉取并解析 BMS 难度表的能力:
4//! - 获取网页并从 HTML 提取 bmstable 头部地址(如有);
5//! - 下载并解析头部 JSON;
6//! - 根据头部中的 `data_url` 下载谱面数据并解析;
7//! - 返回包含表头与谱面集合的 `BmsTable`。
8//!
9//! # 示例
10//!
11//! ```rust,no_run
12//! # #[tokio::main]
13//! # async fn main() -> anyhow::Result<()> {
14//! use bms_table::fetch::reqwest::fetch_bms_table;
15//! let table = fetch_bms_table("https://stellabms.xyz/sl/table.html").await?;
16//! assert!(!table.data.charts.is_empty());
17//! # Ok(())
18//! # }
19//! ```
20#![cfg(feature = "reqwest")]
21
22use anyhow::{anyhow, Result};
23use serde_json::Value;
24use url::Url;
25
26/// 从网页或头部 JSON 源拉取并解析完整的 BMS 难度表。
27///
28/// # 参数
29///
30/// - `web_url`:网页地址或直接指向头部 JSON 的地址。
31///
32/// # 返回
33///
34/// 解析后的 [`crate::BmsTable`],包含表头与谱面数据。
35///
36/// # 错误
37///
38/// - 网络请求失败(连接失败、超时等)
39/// - 响应内容无法解析为 HTML/JSON 或结构不符合预期
40/// - 头部 JSON 未包含 `data_url` 字段或其类型不正确
41pub async fn fetch_bms_table(web_url: &str) -> Result<crate::BmsTable> {
42    let web_url = Url::parse(web_url)?;
43    let web_response = reqwest::Client::new()
44        .get(web_url.clone())
45        .send()
46        .await
47        .map_err(|e| anyhow!("When fetching web: {e}"))?
48        .text()
49        .await
50        .map_err(|e| anyhow!("When parsing web response: {e}"))?;
51    let (header_url, header_json) = match crate::fetch::get_web_header_json_value(&web_response)? {
52        crate::fetch::HeaderQueryContent::Url(header_url_string) => {
53            let header_url = web_url.join(&header_url_string)?;
54            let header_response = reqwest::Client::new()
55                .get(header_url.clone())
56                .send()
57                .await
58                .map_err(|e| anyhow!("When fetching header: {e}"))?;
59            let header_response_string = header_response
60                .text()
61                .await
62                .map_err(|e| anyhow!("When parsing header response: {e}"))?;
63            let crate::fetch::HeaderQueryContent::Json(header_json) =
64                crate::fetch::get_web_header_json_value(&header_response_string)?
65            else {
66                return Err(anyhow!(
67                    "Cycled header found. web_url: {web_url}, header_url: {header_url_string}"
68                ));
69            };
70            (header_url, header_json)
71        }
72        crate::fetch::HeaderQueryContent::Json(value) => (web_url, value),
73    };
74    let data_url_str = header_json
75        .get("data_url")
76        .ok_or_else(|| anyhow!("\"data_url\" not found in header json!"))?
77        .as_str()
78        .ok_or_else(|| anyhow!("\"data_url\" is not a string!"))?;
79    let data_url = header_url.join(data_url_str)?;
80    let data_response = reqwest::Client::new()
81        .get(data_url)
82        .send()
83        .await
84        .map_err(|e| anyhow!("When fetching web: {e}"))?
85        .text()
86        .await
87        .map_err(|e| anyhow!("When parsing web response: {e}"))?;
88    let data_json: Value = serde_json::from_str(&data_response)?;
89    // 直接使用库内反序列化生成 BmsTable
90    let header: crate::BmsTableHeader = serde_json::from_value(header_json)
91        .map_err(|e| anyhow!("When parsing header json: {e}"))?;
92    let data: crate::BmsTableData =
93        serde_json::from_value(data_json).map_err(|e| anyhow!("When parsing data json: {e}"))?;
94    Ok(crate::BmsTable { header, data })
95}