Skip to main content

lyrics_next/
client.rs

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/// 歌词抓取器
22#[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
39// 公共基础结构
40struct 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    // 添加重试机制
60    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
80/// 初始client
81pub fn get_lyrics_client() -> &'static LyricsClient {
82    static CLIENT: OnceLock<LyricsClient> = OnceLock::new();
83    CLIENT.get_or_init(LyricsClient::new)
84}
85
86// 统一调用入口
87pub 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                    //if self.validate_lyric(song, &lyric) {
138                    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                    // }
148                }
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        // 额外检查时长标签(如果有)
185        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
191// 检测两者是否类似
192
193pub 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}