lavende-core 0.1.15

Core in-process Discord voice connection and playback engine
Documentation
use async_trait::async_trait;
use serde_json::{Value, json};

use super::{
    LyricsProvider,
    utils::{self, TokenManager},
};
use crate::{
    config::HttpProxyConfig,
    protocol::{
        models::{LyricsData, LyricsLine},
        tracks::TrackInfo,
    },
};

pub struct DeezerProvider {
    client: reqwest::Client,
    token_manager: TokenManager,
}

impl Default for DeezerProvider {
    fn default() -> Self {
        Self::new(None)
    }
}

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

        if let Some(proxy_cfg) = proxy_config {
            if let Some(p_obj) = proxy_cfg
                .url
                .as_ref()
                .and_then(|u| reqwest::Proxy::all(u).ok())
            {
                let mut proxy_obj = p_obj;
                if let (Some(user), Some(pass)) = (&proxy_cfg.username, &proxy_cfg.password) {
                    proxy_obj = proxy_obj.basic_auth(user, pass);
                }
                client_builder = client_builder.proxy(proxy_obj);
                tracing::info!("Deezer Lyrics Provider: HTTP Proxy configured");
            }
        }

        Self {
            client: client_builder
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            token_manager: TokenManager::new(300_000), // 5 minutes
        }
    }

    async fn get_jwt(&self) -> Option<String> {
        self.token_manager
            .get_token(|| async {
                let resp = self
                    .client
                    .get("https://auth.deezer.com/login/anonymous?jo=p&rto=c")
                    .send()
                    .await
                    .ok()?;

                let data: Value = resp.json().await.ok()?;
                data.get("jwt")
                    .and_then(|t| t.as_str())
                    .map(|s| s.to_owned())
            })
            .await
    }

    async fn search_track(&self, title: &str, author: &str) -> Option<String> {
        let query = format!("{} {}", title, author);
        let resp = self
            .client
            .get("https://api.deezer.com/2.0/search")
            .query(&[("q", query.as_str()), ("limit", "1")])
            .send()
            .await
            .ok()?;

        let data: Value = resp.json().await.ok()?;
        data.get("data")
            .and_then(|d| d.as_array())
            .and_then(|a| a.first())
            .and_then(|t| t.get("id"))
            .and_then(|id| id.as_u64())
            .map(|id| id.to_string())
    }
}

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

    async fn load_lyrics(&self, track: &TrackInfo) -> Option<LyricsData> {
        let jwt = self.get_jwt().await?;

        let title = utils::clean_text(&track.title);
        let author = utils::clean_text(&track.author);

        let track_id = if track.source_name == "deezer" {
            track.identifier.clone()
        } else {
            self.search_track(&title, &author).await?
        };

        let query = r#"
        query GetLyrics($trackId: String!) {
          track(trackId: $trackId) {
            id
            lyrics {
              id
              text
              ...SynchronizedWordByWordLines
              ...SynchronizedLines
              licence
              copyright
              writers
              __typename
            }
            __typename
          }
        }

        fragment SynchronizedWordByWordLines on Lyrics {
          id
          synchronizedWordByWordLines {
            start
            end
            words {
              start
              end
              word
              __typename
            }
            __typename
          }
          __typename
        }

        fragment SynchronizedLines on Lyrics {
          id
          synchronizedLines {
            lrcTimestamp
            line
            lineTranslated
            milliseconds
            duration
            __typename
          }
          __typename
        }
        "#;

        let body = json!({
            "operationName": "GetLyrics",
            "variables": { "trackId": track_id },
            "query": query
        });

        let resp = self
            .client
            .post("https://pipe.deezer.com/api")
            .header("Authorization", format!("Bearer {}", jwt))
            .json(&body)
            .send()
            .await
            .ok()?;

        let data: Value = resp.json().await.ok()?;
        let lyrics = data
            .get("data")
            .and_then(|d| d.get("track"))
            .and_then(|t| t.get("lyrics"))?;

        let mut lines = Vec::new();
        let mut synced = false;

        if let Some(swb) = lyrics
            .get("synchronizedWordByWordLines")
            .and_then(|l| l.as_array())
            .filter(|a| !a.is_empty())
        {
            synced = true;
            for line in swb {
                let start = line.get("start").and_then(|v| v.as_u64()).unwrap_or(0);
                let end = line.get("end").and_then(|v| v.as_u64()).unwrap_or(0);
                let words = line.get("words").and_then(|v| v.as_array());

                let text = words
                    .map(|w| {
                        w.iter()
                            .map(|s| s.get("word").and_then(|v| v.as_str()).unwrap_or(""))
                            .collect::<Vec<_>>()
                            .join(" ")
                    })
                    .unwrap_or_default();

                lines.push(LyricsLine {
                    text,
                    timestamp: start,
                    duration: end.saturating_sub(start),
                });
            }
        }

        if !synced {
            if let Some(sl) = lyrics
                .get("synchronizedLines")
                .and_then(|l| l.as_array())
                .filter(|a| !a.is_empty())
            {
                synced = true;
                for line in sl {
                    let text = line
                        .get("line")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_owned();
                    let timestamp = line
                        .get("milliseconds")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    let duration = line.get("duration").and_then(|v| v.as_u64()).unwrap_or(0);

                    lines.push(LyricsLine {
                        text,
                        timestamp,
                        duration,
                    });
                }
            }
        }

        let full_text = if let Some(text) = lyrics.get("text").and_then(|v| v.as_str()) {
            text.to_string()
        } else if !lines.is_empty() {
            lines
                .iter()
                .map(|l| l.text.as_str())
                .collect::<Vec<_>>()
                .join("\n")
        } else {
            return None;
        };

        Some(LyricsData {
            name: track.title.clone(),
            author: track.author.clone(),
            provider: "deezer".to_string(),
            text: full_text,
            lines: if synced { Some(lines) } else { None },
        })
    }
}