Skip to content

Migration 0.3

ChouChiu edited this page Sep 23, 2026 · 1 revision

从 0.2 升级到 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 }

搜索层改为返回 SearchError

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}"),
}

SearchError 的变体

变体 含义
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}"),
}

base_api 的请求函数收敛

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。

SyllableItem 不再实现 PartialEq

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() 的签名保持不变

Clone this wiki locally