lavende-core 0.1.15

Core in-process Discord voice connection and playback engine
Documentation
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose};
use hmac::{Hmac, Mac};
use serde_json::Value;
use sha2::Sha256;

use super::{LyricsProvider, utils};
use crate::{
    config::{HttpProxyConfig, lyrics::YandexLyricsConfig},
    protocol::{models::LyricsData, tracks::TrackInfo},
};

pub struct YandexProvider {
    client: reqwest::Client,
    access_token: Option<String>,
}

impl YandexProvider {
    pub fn new(config: &YandexLyricsConfig, proxy_config: Option<&HttpProxyConfig>) -> Self {
        let mut client_builder = reqwest::Client::builder();

        if let Some(proxy_cfg) = proxy_config {
            if let Some(url) = &proxy_cfg.url {
                if let Ok(mut proxy_obj) = reqwest::Proxy::all(url) {
                    if let Some(user) = &proxy_cfg.username {
                        if let Some(pass) = &proxy_cfg.password {
                            proxy_obj = proxy_obj.basic_auth(user, pass);
                        }
                    }
                    client_builder = client_builder.proxy(proxy_obj);
                    tracing::info!("Yandex Lyrics Provider: HTTP Proxy configured");
                } else {
                    tracing::warn!("Yandex Lyrics Provider: Invalid proxy URL: {}", url);
                }
            }
        }

        Self {
            client: client_builder
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            access_token: config.access_token.clone(),
        }
    }

    fn create_sign(&self, track_id: &str) -> Option<(String, u64)> {
        let ts = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .ok()?
            .as_secs();
        let message = format!("{}{}", track_id, ts);
        let mut mac = Hmac::<Sha256>::new_from_slice(b"p93jhgh689SBReK6ghtw62")
            .expect("HMAC-SHA256 key size is valid");
        mac.update(message.as_bytes());
        let result = mac.finalize();
        let sign = general_purpose::STANDARD.encode(result.into_bytes());
        Some((urlencoding::encode(&sign).into_owned(), ts))
    }
}

#[async_trait]
impl LyricsProvider for YandexProvider {
    fn name(&self) -> &'static str {
        "yandex"
    }

    async fn load_lyrics(&self, track: &TrackInfo) -> Option<LyricsData> {
        let token = self.access_token.as_ref()?;
        let title = utils::clean_text(&track.title);
        let author = utils::clean_text(&track.author);

        let track_id = if track.source_name == "yandexmusic" {
            track.identifier.clone()
        } else {
            let query = format!("{} {}", title, author);
            let search_url = format!(
                "https://api.music.yandex.net/search?text={}&type=track&page=0",
                urlencoding::encode(&query)
            );
            let search_resp = self
                .client
                .get(search_url)
                .header("Authorization", format!("OAuth {}", token))
                .header("X-Yandex-Music-Client", "YandexMusicAndroid/24023621")
                .send()
                .await
                .ok()?;

            let search_body: Value = search_resp.json().await.ok()?;
            search_body["result"]["tracks"]["results"]
                .as_array()?
                .first()?
                .get("id")?
                .as_i64()?
                .to_string()
        };

        let (sign, ts) = self.create_sign(&track_id)?;
        let url = format!(
            "https://api.music.yandex.net/tracks/{}/lyrics?format=LRC&timeStamp={}&sign={}",
            track_id, ts, sign
        );

        let resp = self
            .client
            .get(url)
            .header("Authorization", format!("OAuth {}", token))
            .header("X-Yandex-Music-Client", "YandexMusicAndroid/24023621")
            .send()
            .await
            .ok()?;

        let body: Value = resp.json().await.ok()?;
        let download_url = body["result"]["downloadUrl"].as_str()?;

        let lrc_resp = self
            .client
            .get(download_url)
            .header("Authorization", format!("OAuth {}", token))
            .send()
            .await
            .ok()?;

        let lrc_text = lrc_resp.text().await.ok()?;
        let lines = utils::parse_lrc(&lrc_text);

        if lines.is_empty() {
            return None;
        }

        Some(LyricsData {
            name: track.title.clone(),
            author: track.author.clone(),
            provider: "yandex".to_string(),
            text: lines
                .iter()
                .map(|l| l.text.as_str())
                .collect::<Vec<_>>()
                .join("\n"),
            lines: Some(lines),
        })
    }
}