koan_core/remote/
lrclib.rs1use serde::Deserialize;
2use thiserror::Error;
3
4const USER_AGENT: &str = "koan-music/0.3.0";
5
6#[derive(Debug, Error)]
7pub enum LrclibError {
8 #[error("http error: {0}")]
9 Http(#[from] reqwest::Error),
10 #[error("not found")]
11 NotFound,
12}
13
14#[derive(Debug, Clone, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct LrclibResponse {
17 pub synced_lyrics: Option<String>,
18 pub plain_lyrics: Option<String>,
19}
20
21pub fn get_lyrics(
27 artist: &str,
28 title: &str,
29 album: &str,
30 duration_secs: u64,
31) -> Result<LrclibResponse, LrclibError> {
32 let client = reqwest::blocking::Client::builder()
33 .user_agent(USER_AGENT)
34 .build()?;
35
36 let resp = client
38 .get("https://lrclib.net/api/get")
39 .query(&[
40 ("artist_name", artist),
41 ("track_name", title),
42 ("album_name", album),
43 ])
44 .query(&[("duration", &duration_secs.to_string())])
45 .send()?;
46
47 if resp.status() != reqwest::StatusCode::NOT_FOUND {
48 let body: LrclibResponse = resp.error_for_status()?.json()?;
49 return Ok(body);
50 }
51
52 let query = format!("{} {}", artist, title);
54 let resp = client
55 .get("https://lrclib.net/api/search")
56 .query(&[("q", query.as_str())])
57 .send()?;
58
59 let results: Vec<LrclibResponse> = resp.error_for_status()?.json()?;
60 results.into_iter().next().ok_or(LrclibError::NotFound)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn test_deserialize_lrclib_response() {
69 let json = r#"{
70 "id": 123,
71 "trackName": "Test",
72 "artistName": "Artist",
73 "albumName": "Album",
74 "duration": 240,
75 "instrumental": false,
76 "plainLyrics": "Hello world\nSecond line",
77 "syncedLyrics": "[00:12.00]Hello world\n[00:17.20]Second line"
78 }"#;
79 let resp: LrclibResponse = serde_json::from_str(json).unwrap();
80 assert_eq!(
81 resp.plain_lyrics.as_deref(),
82 Some("Hello world\nSecond line")
83 );
84 assert!(resp.synced_lyrics.unwrap().contains("[00:12.00]"));
85 }
86
87 #[test]
88 fn test_deserialize_lrclib_response_null_lyrics() {
89 let json = r#"{
90 "id": 456,
91 "trackName": "Instrumental",
92 "artistName": "Artist",
93 "albumName": "Album",
94 "duration": 180,
95 "instrumental": true,
96 "plainLyrics": null,
97 "syncedLyrics": null
98 }"#;
99 let resp: LrclibResponse = serde_json::from_str(json).unwrap();
100 assert!(resp.plain_lyrics.is_none());
101 assert!(resp.synced_lyrics.is_none());
102 }
103}