1use std::sync::OnceLock;
2
3use async_trait::async_trait;
4use kugou::KugouFetcher;
5use netease::NeteaseFetcher;
6use qqmusic::QQMusicFetcher;
7use reqwest::RequestBuilder;
8use serde::de::DeserializeOwned;
9use tracing::{debug, info, warn};
10
11use crate::{
12 cache::CacheManager, config::get_config, error::LyricsError, song::SongInfo,
13 utils::normalize_text,
14};
15
16mod kugou;
17mod netease;
18mod ovh;
19mod qqmusic;
20
21#[async_trait]
23trait LyricsFetcher: Send + Sync {
24 async fn search_lyric(&self, song: &SongInfo) -> Result<Vec<LyricsItem>, LyricsError>;
25 async fn download_lyric(&self, item: &LyricsItem) -> Result<String, LyricsError>;
26 async fn fetch_lyric(&self, song: &SongInfo) -> Result<String, LyricsError>;
27 fn source_name(&self) -> &'static str;
28}
29
30#[derive(Debug, Clone)]
31pub struct LyricsItem {
32 pub source: String,
33 pub title: String,
34 pub artist: String,
35 pub album: String,
36 pub params: Vec<(String, String)>,
37}
38
39struct BaseFetcher {
41 client: reqwest::Client,
42 retries: u8,
43}
44
45impl Default for BaseFetcher {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl BaseFetcher {
52 fn new() -> Self {
53 Self {
54 client: reqwest::Client::new(),
55 retries: 3,
56 }
57 }
58
59 async fn fetch_with_retry<T: DeserializeOwned>(
61 &self,
62 request: RequestBuilder,
63 ) -> Result<T, LyricsError> {
64 let mut attempt = 0;
65 loop {
66 let response = request.try_clone().unwrap().send().await;
67 debug!("REQUEST: {:?} \n RESPONSE: {:?}", request, response);
68 match response {
69 Ok(res) => return Ok(res.json::<T>().await?),
70 Err(_e) if attempt < self.retries => {
71 tokio::time::sleep(std::time::Duration::from_secs(1 << attempt)).await;
72 attempt += 1;
73 }
74 Err(e) => return Err(e.into()),
75 }
76 }
77 }
78}
79
80pub fn get_lyrics_client() -> &'static LyricsClient {
82 static CLIENT: OnceLock<LyricsClient> = OnceLock::new();
83 CLIENT.get_or_init(LyricsClient::new)
84}
85
86pub struct LyricsClient {
88 fetchers: Vec<Box<dyn LyricsFetcher>>,
89 pub cache: CacheManager,
90}
91
92impl LyricsClient {
93 fn new() -> Self {
94 let mut fetchers: Vec<Box<dyn LyricsFetcher>> = Vec::new();
95
96 let config = &get_config().read().unwrap().sources;
97
98 if config.kugou {
99 fetchers.push(Box::new(KugouFetcher::default()));
100 }
101
102 if config.netease {
103 fetchers.push(Box::new(NeteaseFetcher::default()));
104 }
105
106 if config.qq {
107 fetchers.push(Box::new(QQMusicFetcher::default()));
108 }
109
110 Self {
111 fetchers,
112 cache: CacheManager::new(),
113 }
114 }
115
116 pub async fn get_search(&self, song: &SongInfo) -> Result<Vec<LyricsItem>, LyricsError> {
117 let mut list = Vec::new();
118
119 for fetcher in &self.fetchers {
120 let mut sl = fetcher.search_lyric(song).await.unwrap_or_default();
121 list.append(&mut sl);
122 }
123
124 Ok(list)
125 }
126
127 pub async fn get_lyrics(&self, song: &SongInfo) -> Result<String, LyricsError> {
128 if let Some(cached) = self.cache.get(song).await {
129 info!("Load local lyric file: {} - {}", song.artist, song.title);
130 return Ok(cached);
131 }
132
133 for fetcher in &self.fetchers {
134 info!("Trying source: {}", fetcher.source_name());
135 match fetcher.fetch_lyric(song).await {
136 Ok(lyric) => {
137 info!(
139 "Successfully fetched {} from {}",
140 song.title,
141 fetcher.source_name()
142 );
143 self.cache
144 .store(song, fetcher.source_name(), &lyric)
145 .await?;
146 return Ok(lyric);
147 }
149 Err(e) => warn!("{} failed: {}", fetcher.source_name(), e),
150 }
151 }
152 Err(LyricsError::NoLyricsFound)
153 }
154
155 pub async fn download(&self, song: &SongInfo, item: &LyricsItem) -> Result<(), LyricsError> {
156 for fetcher in &self.fetchers {
157 if fetcher.source_name() == item.source {
158 match fetcher.download_lyric(item).await {
159 Ok(lyric) => {
160 info!(
161 "Successfully download {} from {}",
162 song.title,
163 fetcher.source_name()
164 );
165 self.cache
166 .store(song, fetcher.source_name(), &lyric)
167 .await?;
168 return Ok(());
169 }
170 Err(e) => warn!("{} failed: {}", fetcher.source_name(), e),
171 }
172 }
173 }
174
175 Err(LyricsError::NoLyricsFound)
176 }
177
178 #[allow(dead_code)]
179 fn validate_lyric(&self, song: &SongInfo, lyric: &str) -> bool {
180 let normalized_lyric = normalize_text(lyric);
181 let has_title = normalized_lyric.contains(&normalize_text(&song.title));
182 let has_artist = normalized_lyric.contains(&normalize_text(&song.artist));
183
184 let has_duration = lyric.contains(&format!("{:0.1}", song.duration));
186
187 has_title && has_artist && (song.duration <= 0.0 || has_duration)
188 }
189}
190
191pub fn get_first(list: Vec<LyricsItem>, song: &SongInfo) -> Result<LyricsItem, LyricsError> {
194 let list: Vec<LyricsItem> = list
195 .into_iter()
196 .filter(|s| s.title == song.title || s.title.contains(&song.title))
197 .collect();
198
199 if list.is_empty() {
200 return Err(LyricsError::NoLyricsFound);
201 }
202
203 let list: Vec<LyricsItem> = if !song.artist.is_empty() {
204 list.into_iter()
205 .filter(|s| {
206 if song.artist.is_empty() {
207 true
208 } else {
209 let a = s.artist.to_lowercase();
210 let b = song.artist.to_lowercase();
211 a == b || a.contains(&b)
212 }
213 })
214 .collect()
215 } else {
216 list
217 };
218
219 if list.is_empty() {
220 return Err(LyricsError::NoLyricsFound);
221 }
222
223 let list: Vec<LyricsItem> = if !song.album.is_empty() {
224 list.into_iter()
225 .filter(|s| {
226 if song.album.is_empty() {
227 true
228 } else {
229 let a = s.album.to_lowercase();
230 let b = song.album.to_lowercase();
231 a == b || a.contains(&b)
232 }
233 })
234 .collect()
235 } else {
236 list
237 };
238
239 if list.is_empty() {
240 return Err(LyricsError::NoLyricsFound);
241 }
242
243 let first = list.first().ok_or(LyricsError::NoLyricsFound)?;
244 Ok(first.clone())
245}