-
Notifications
You must be signed in to change notification settings - Fork 1
Migration 0.3
0.3.0 是破坏性版本。六个 crate 统一为 0.3.0,搜索层改为返回类型化错误,SyllableItem 的相等语义被移除。
[dependencies]
lyrics-helper = "0.3"仅需离线解析时禁用默认 feature:
[dependencies]
lyrics-helper = { version = "0.3", default-features = false }0.2 的搜索层把每个失败都压成 None:base_api 的请求辅助函数一律以 .ok()? 结尾,provider 内部的类型化错误也在公开入口被拍平。调用方因此分不清「这首歌没有」「命中 captcha」「被限流」「网络不通」。
0.3 引入 crate 级错误类型 SearchError,门面 crate 已顶层再导出:
use lyrics_helper::models::TrackMetadata;
use lyrics_helper::searchers::netease::NeteaseSearcher;
use lyrics_helper::searchers::search_for_best_result;
use lyrics_helper::SearchError;
let mut track = TrackMetadata::new();
track.title = Some("晴天".to_string());
track.artist = Some("周杰伦".to_string());
track.ensure_artists();
match search_for_best_result(&NeteaseSearcher, &track).await {
Ok(Some(best)) => println!("{}", best.title),
Ok(None) => println!("没有搜到"), // 搜索成功,但没有结果
Err(SearchError::Status(429)) => println!("被限流"),
Err(SearchError::Captcha) => println!("命中验证码"),
Err(SearchError::Http(error)) if error.is_timeout() => println!("超时"),
Err(error) => println!("搜索失败: {error}"),
}| 变体 | 含义 |
|---|---|
Http(reqwest::Error) |
网络请求失败或响应体按目标类型解码失败;可用 is_timeout() / is_connect() / is_decode() 进一步区分 |
Json(serde_json::Error) |
响应文本不是合法 JSON |
Status(u16) |
服务端返回非 2xx 状态码(429 限流、401 凭证失效等) |
Api(String) |
HTTP 成功但业务层失败(例如 QQ 音乐的 code != 0),消息里带原始错误码 |
Captcha |
命中验证码风控(Musixmatch 401 + captcha hint) |
Payload(String) |
响应内容不符合预期格式(base64 / UTF-8 解码失败等) |
InvalidConfig(String) |
传入的配置非法 |
SearchError 实现了 Display 与 std::error::Error(source() 保留底层 reqwest / serde_json 错误)。
「没有数据」仍然用 Option / 空集合表达,只有真正的失败才是 Err:
-
Ok(None):请求成功,但该曲目没有这种歌词(例如没有逐字歌词) -
Ok(vec![]):请求成功,但没有匹配到搜索结果 -
Err(_):请求失败,或平台返回业务错误码
Searcher trait 的方法(lyrics-search/src/searchers/searcher.rs):
async fn search_for_results_str(
&self,
search_string: &str,
) -> Result<Vec<SearchResult>, SearchError>;
// search_for_results 在 trait 中有默认实现:用 title / artist / album 拼出搜索串
async fn search_for_results(
&self,
track: &TrackMetadata,
) -> Result<Vec<SearchResult>, SearchError>searchers 模块的三个驱动函数(lyrics-search/src/searchers/mod.rs):
pub async fn search_with_refinement(
searcher: &dyn Searcher,
track: &TrackMetadata,
full_search: bool,
) -> Result<Vec<SearchResult>, SearchError>pub async fn search_for_best_result(
searcher: &dyn Searcher,
track: &TrackMetadata,
) -> Result<Option<SearchResult>, SearchError>pub async fn search_for_best_result_with_match(
searcher: &dyn Searcher,
track: &TrackMetadata,
minimum_match: MatchType,
) -> Result<Option<SearchResult>, SearchError>三个驱动函数的两段式与渐进式策略、结果排序、匹配度阈值判定都与 0.2 一致,只是「全部查询层都没有结果」时代替 None 返回 Err(优先返回第一次的错误),这样「没搜到」和「搜不动」不再混为一谈。
逐字/逐行歌词入口(以网易云为例):
use lyrics_helper::providers::web::netease;
// 逐行歌词:请求成功但某一类歌词缺失时,元组里对应元素为 None
match netease::api::get_lyrics(423997333).await {
Ok((Some(lyric), _translation)) => println!("{lyric}"),
Ok((None, _)) => println!("该曲目没有逐行歌词"),
Err(error) => println!("取歌词失败: {error}"),
}
// 逐字歌词:Ok(None) 表示该曲目没有逐字歌词
match netease::api::get_syllable_lyrics(423997333).await {
Ok(Some(lyrics)) => println!("{:?}", lyrics.yrc.is_some()),
Ok(None) => println!("该曲目没有逐字歌词"),
Err(error) => println!("请求失败: {error}"),
}Musixmatch:
-
MusixmatchError已删除,内部与公开入口统一使用SearchError(命中 captcha 仍是短路,不会进入重试循环) -
api::set_options不再 panic:非法配置返回Err(SearchError::InvalidConfig(_)) -
api::get_token返回Result<Option<String>, SearchError>,Ok(None)表示服务端没给出可用 token
use lyrics_helper::providers::web::musixmatch::{api, api_options::ApiOptions};
use lyrics_helper::SearchError;
let mut options = ApiOptions::desktop();
options.app_id = String::new(); // 非法配置
match api::set_options(options).await {
Ok(()) => println!("配置已生效"),
Err(SearchError::InvalidConfig(message)) => println!("配置非法: {message}"),
Err(error) => println!("{error}"),
}0.2 的 7 个请求辅助函数(get_json、get_json_with_headers、post_json、post_json_with_headers、post_json_raw_with_headers、post_form、post_form_raw_with_headers)差别只有 method × body 类型 × 解码方式,现在统一为 3 个发送函数 + 2 个终结方法:
三个发送函数只负责把请求发出去并拿回原始响应:
pub async fn send(
method: Method,
url: &str,
headers: &[(&str, &str)],
) -> Result<Response, SearchError>pub async fn send_json(
url: &str,
body: &impl Serialize,
headers: &[(&str, &str)],
) -> Result<Response, SearchError>pub async fn send_form(
url: &str,
form: &[(&str, &str)],
headers: &[(&str, &str)],
) -> Result<Response, SearchError>两个终结方法负责解码响应体:
pub async fn json<T: DeserializeOwned>(response: Response) -> Result<T, SearchError>pub async fn text(response: Response) -> Result<String, SearchError>非 2xx 状态码由 json / text 统一返回 SearchError::Status,不再依赖「拿错误页正文去反序列化然后失败」这种间接表现;因此需要把 404 当作「没有这首歌」的调用方(如 LRCLIB)要先自己判状态码:
use lyrics_helper::providers::web::base_api;
use lyrics_helper::providers::web::base_api::{Method, StatusCode};
use lyrics_helper::providers::web::lrclib::response::SearchResultItem;
let response = base_api::send(Method::GET, "https://lrclib.net/api/search?track_name=Yesterday", &[])
.await
.expect("请求失败");
if response.status() == StatusCode::NOT_FOUND {
// 404 这类「没有这首歌」由调用方自己判定
return;
}
let items: Vec<SearchResultItem> = base_api::json(response).await.expect("解码失败");签名里用到的 Method、StatusCode、Response 已从 base_api 再导出,调用方不必自己依赖 reqwest。
0.2 的 SyllableItem 只比较 start_time / end_time、完全忽略文本,两个文本不同的音节只要时间相同就被判为相等,contains / dedup / position / assert_eq! 都会因此给出违反直觉的结果。上游 C# 的 ISyllableInfo 只有 Text / StartTime / EndTime / Duration,本来也没有任何相等语义,0.3 直接删除了这个实现;仓库内它本来也没有使用者。
需要按时间比较时显式写:
use lyrics_helper::{SyllableInfo, SyllableItem};
let a = SyllableItem::from(SyllableInfo::new("晴".to_string(), 1000, 1500));
let b = SyllableItem::from(SyllableInfo::new("天".to_string(), 1000, 1500));
// 0.2 里 a == b 为 true(只比时间),0.3 起没有 PartialEq,必须显式比较
assert!(a.start_time() == b.start_time() && a.end_time() == b.end_time());LineInfo、LyricsData、SyllableInfo、FullSyllableInfo 本来就没有实现 PartialEq,所以这次删除不会影响上层结构。LineInfo 的 PartialEq / Ord(只比开始时间)保留不动,它是排序用的,对应上游 C# 的 IComparable。
- 示例已同步:
search_test/search_lyrics_test会直接打印失败原因,例如缺 token 时显示「服务端返回 HTTP 401」,命中风控时显示「命中验证码风控,需要人工处理」 -
musixmatch::api::options()、get_user_token()、set_user_token()的签名保持不变