use async_trait::async_trait;
use base64::{Engine, prelude::BASE64_STANDARD};
use serde::Deserialize;
use tracing::debug;
use super::{BaseFetcher, LyricsFetcher, LyricsItem};
use crate::{client::get_first, error::LyricsError, song::SongInfo};
#[derive(Debug, Deserialize)]
struct SearchResponse {
data: SearchData,
}
#[derive(Debug, Deserialize)]
struct SearchData {
info: Vec<Song>,
}
#[derive(Debug, Deserialize)]
struct Song {
hash: String,
album_id: String,
album_name: String,
singername: String,
songname_original: String,
}
#[derive(Debug, Deserialize)]
struct LyricResponse {
candidates: Vec<Candidate>,
}
#[derive(Debug, Deserialize)]
struct Candidate {
accesskey: String,
download_id: String,
singer: String,
song: String,
}
#[derive(Debug, Deserialize)]
struct LyricData {
content: String,
}
#[derive(Default)]
pub(super) struct KugouFetcher {
base: BaseFetcher,
}
impl KugouFetcher {
fn decode_lyric(&self, encrypted: &str) -> Result<String, LyricsError> {
let bytes = BASE64_STANDARD.decode(encrypted)?;
let re = String::from_utf8(bytes).map_err(|_| LyricsError::LyricsDecodeError)?;
if re.is_empty() {
return Err(LyricsError::NoLyricsFound);
}
Ok(re)
}
}
#[async_trait]
impl LyricsFetcher for KugouFetcher {
async fn search_lyric(&self, song: &SongInfo) -> Result<Vec<LyricsItem>, LyricsError> {
let search_url = "http://mobilecdn.kugou.com/api/v3/search/song";
let request = self.base.client.get(search_url).query(&[
(
"keyword",
format!("{} {}", song.title, song.artist).as_str(),
),
("page", "1"),
("pagesize", "1"),
]);
let data: SearchResponse = self.base.fetch_with_retry(request).await?;
debug!("song json: {:?}", data);
let search = data
.data
.info
.into_iter()
.filter(|s| {
s.songname_original == song.title || s.songname_original.contains(&song.title)
})
.filter(|s| {
if song.artist.is_empty() {
true
} else {
let a = s.singername.to_lowercase();
let b = song.artist.to_lowercase();
a == b || a.contains(&b)
}
})
.find(|s| {
if song.album.is_empty() {
true
} else {
let a = s.album_name.to_lowercase();
let b = song.album.to_lowercase();
a == b || a.contains(&b)
}
})
.ok_or(LyricsError::NoLyricsFound)?;
debug!("song hash: {} {}", search.album_id, search.hash);
let lyric_url = "http://krcs.kugou.com/search";
let request = self
.base
.client
.get(lyric_url)
.query(&[
("hash", search.hash.as_str()),
("album_id", search.album_id.as_str()),
("ver", "1"),
("client", "pc"),
("man", "yes"),
])
.header("User-Agent", "Mozilla/5.0");
let data: LyricResponse = self.base.fetch_with_retry(request).await?;
debug!("lyric list: {:?}", data);
let list: Vec<LyricsItem> = data
.candidates
.into_iter()
.map(|s| {
let source = self.source_name().into();
let title = s.song;
let artist = s.singer;
let album = search.album_name.clone();
let params = vec![
("accesskey".to_string(), s.accesskey),
("id".to_string(), s.download_id),
];
LyricsItem {
source,
title,
artist,
album,
params,
}
})
.collect();
debug!("Get List: {:?}", list);
if !list.is_empty() {
Ok(list)
} else {
Err(LyricsError::NoLyricsFound)
}
}
async fn download_lyric(&self, item: &LyricsItem) -> Result<String, LyricsError> {
let mut params = item.params.clone();
params.append(&mut vec![
("ver".to_string(), "1".to_string()),
("client".to_string(), "pc".to_string()),
("fmt".to_string(), "lrc".to_string()),
("charset".to_string(), "utf8".to_string()),
]);
let lyric_download_url = "http://lyrics.kugou.com/download";
let request = self
.base
.client
.get(lyric_download_url)
.query(¶ms)
.header("User-Agent", "Mozilla/5.0");
let data: LyricData = self.base.fetch_with_retry(request).await?;
debug!("lyric: {:?}", data);
let decoded = self.decode_lyric(&data.content)?;
Ok(decoded)
}
async fn fetch_lyric(&self, song: &SongInfo) -> Result<String, LyricsError> {
debug!("kugou start ");
let list = self.search_lyric(song).await?;
let item = get_first(list, song)?;
debug!("Get song: {:?} info: {:?}", item, song);
self.download_lyric(&item).await
}
fn source_name(&self) -> &'static str {
"Kugou"
}
}