lavende-core 0.1.7

Core in-process Discord voice connection and playback engine
Documentation
use crate::{
    config::sources::FloweryConfig,
    protocol::tracks::{LoadResult, Track, TrackInfo},
    sources::{SourcePlugin, http::HttpTrack, playable_track::BoxedTrack},
};
use async_trait::async_trait;
use regex::Regex;
use std::sync::Arc;
use tracing::debug;
pub struct FlowerySource {
    config: FloweryConfig,
    search_prefixes: Vec<String>,
    url_pattern: Regex,
}
impl FlowerySource {
    pub fn new(config: FloweryConfig) -> Self {
        Self {
            config,
            search_prefixes: vec!["ftts:".to_string()],
            url_pattern: Regex::new(r"(?i)^ftts://").unwrap(),
        }
    }
    fn build_track_info(&self, text: &str, identifier: &str, url: &str) -> TrackInfo {
        let title_text = if text.len() > 50 {
            format!("{}...", &text[..47])
        } else {
            text.to_string()
        };
        TrackInfo {
            identifier: identifier.to_string(),
            is_seekable: true,
            author: "Flowery TTS".to_string(),
            length: 0,
            is_stream: false,
            position: 0,
            title: title_text,
            uri: Some(url.to_string()),
            source_name: self.name().to_string(),
            artwork_url: None,
            isrc: None,
        }
    }
    fn build_url(
        &self,
        text: &str,
        params_override: std::collections::HashMap<String, String>,
    ) -> String {
        let mut voice = self.config.voice.clone();
        let mut translate = self.config.translate;
        let mut silence = self.config.silence;
        let mut speed = self.config.speed;
        if !self.config.enforce_config {
            if let Some(v) = params_override.get("voice") {
                voice = v.clone();
            }
            if let Some(t) = params_override.get("translate") {
                translate = t.parse().unwrap_or(translate);
            }
            if let Some(s) = params_override.get("silence") {
                silence = s.parse().unwrap_or(silence);
            }
            if let Some(sp) = params_override.get("speed") {
                speed = sp.parse().unwrap_or(speed);
            }
        }
        let encoded_text = urlencoding::encode(text);
        format!(
            "https://api.flowery.pw/v1/tts?voice={}&text={}&translate={}&silence={}&audio_format=mp3&speed={}",
            urlencoding::encode(&voice),
            encoded_text,
            translate,
            silence,
            speed
        )
    }
    fn parse_query(&self, identifier: &str) -> (String, std::collections::HashMap<String, String>) {
        let mut path_and_query = identifier;
        for prefix in &self.search_prefixes {
            if path_and_query.starts_with(prefix) {
                path_and_query = path_and_query.trim_start_matches(prefix);
                break;
            }
        }
        if path_and_query.starts_with("//") {
            path_and_query = &path_and_query[2..];
        }
        let mut params = std::collections::HashMap::new();
        let text = if let Some(split_idx) = path_and_query.find('?') {
            let decoded_text = urlencoding::decode(&path_and_query[..split_idx])
                .unwrap_or_else(|_| std::borrow::Cow::Borrowed(&path_and_query[..split_idx]))
                .into_owned();
            let query_str = &path_and_query[split_idx + 1..];
            for pair in query_str.split('&') {
                if let Some(eq_idx) = pair.find('=') {
                    let key = &pair[..eq_idx];
                    let value = &pair[eq_idx + 1..];
                    params.insert(
                        urlencoding::decode(key)
                            .unwrap_or(std::borrow::Cow::Borrowed(key))
                            .into_owned(),
                        urlencoding::decode(value)
                            .unwrap_or(std::borrow::Cow::Borrowed(value))
                            .into_owned(),
                    );
                } else if !pair.is_empty() {
                    params.insert(
                        urlencoding::decode(pair)
                            .unwrap_or(std::borrow::Cow::Borrowed(pair))
                            .into_owned(),
                        "".to_string(),
                    );
                }
            }
            decoded_text
        } else {
            urlencoding::decode(path_and_query)
                .unwrap_or(std::borrow::Cow::Borrowed(path_and_query))
                .into_owned()
        };
        (text, params)
    }
}
#[async_trait]
impl SourcePlugin for FlowerySource {
    fn name(&self) -> &str {
        "flowery"
    }
    fn can_handle(&self, identifier: &str) -> bool {
        self.search_prefixes
            .iter()
            .any(|p| identifier.starts_with(p))
            || self.url_pattern.is_match(identifier)
    }
    async fn load(
        &self,
        identifier: &str,
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> LoadResult {
        debug!("Flowery TTS loading: {}", identifier);
        let (text, params) = self.parse_query(identifier);
        if text.trim().is_empty() {
            return LoadResult::Empty {};
        }
        let url = self.build_url(&text, params);
        let info = self.build_track_info(&text, identifier, &url);
        LoadResult::Track(Track::new(info))
    }
    async fn get_track(
        &self,
        identifier: &str,
        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> Option<BoxedTrack> {
        let (text, params) = self.parse_query(identifier);
        let url = self.build_url(&text, params);
        Some(Arc::new(HttpTrack {
            url,
            local_addr: routeplanner.and_then(|rp| rp.get_address()),
            proxy: None,
        }))
    }
    fn search_prefixes(&self) -> Vec<&str> {
        self.search_prefixes.iter().map(|s| s.as_str()).collect()
    }
}