Skip to main content

lavende_core/sources/
google_tts.rs

1use crate::{
2    config::sources::GoogleTtsConfig,
3    protocol::tracks::{LoadResult, Track, TrackInfo},
4    sources::{SourcePlugin, http::HttpTrack, playable_track::BoxedTrack},
5};
6use async_trait::async_trait;
7use regex::Regex;
8use std::sync::Arc;
9use tracing::debug;
10pub struct GoogleTtsSource {
11    config: GoogleTtsConfig,
12    search_prefixes: Vec<String>,
13    url_pattern: Regex,
14}
15impl GoogleTtsSource {
16    pub fn new(config: GoogleTtsConfig) -> Self {
17        Self {
18            config,
19            search_prefixes: vec!["gtts:".to_string(), "speak:".to_string()],
20            url_pattern: Regex::new(r"(?i)^(gtts://|speak://)").unwrap(),
21        }
22    }
23    fn build_track_info(&self, language: &str, text: &str) -> TrackInfo {
24        let title_text = if text.len() > 50 {
25            format!("{}...", &text[..47])
26        } else {
27            text.to_string()
28        };
29        TrackInfo {
30            identifier: format!("gtts://{}:{}", language, text),
31            is_seekable: true,
32            author: "Google TTS".to_string(),
33            length: 0,
34            is_stream: false,
35            position: 0,
36            title: format!("TTS: {}", title_text),
37            uri: Some(self.build_url(language, text)),
38            source_name: self.name().to_string(),
39            artwork_url: None,
40            isrc: None,
41        }
42    }
43    fn build_url(&self, language: &str, text: &str) -> String {
44        let encoded_text = urlencoding::encode(text);
45        format!(
46            "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl={}&total=1&idx=0&textlen={}&client=gtx",
47            encoded_text,
48            language,
49            text.len()
50        )
51    }
52    fn parse_query(&self, identifier: &str) -> (String, String) {
53        let mut path = identifier;
54        for prefix in &self.search_prefixes {
55            if path.starts_with(prefix) {
56                path = path.trim_start_matches(prefix);
57                break;
58            }
59        }
60        if path.starts_with("//") {
61            path = &path[2..];
62        }
63        if let Some(split_idx) = path.find(':') {
64            let lang = &path[..split_idx];
65            let actual_text = &path[split_idx + 1..];
66            if !lang.is_empty() {
67                return (lang.to_string(), actual_text.to_string());
68            }
69        }
70        (self.config.language.clone(), path.to_string())
71    }
72}
73#[async_trait]
74impl SourcePlugin for GoogleTtsSource {
75    fn name(&self) -> &str {
76        "google-tts"
77    }
78    fn can_handle(&self, identifier: &str) -> bool {
79        self.search_prefixes
80            .iter()
81            .any(|p| identifier.starts_with(p))
82            || self.url_pattern.is_match(identifier)
83    }
84    async fn load(
85        &self,
86        identifier: &str,
87        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
88    ) -> LoadResult {
89        debug!("Google TTS loading: {}", identifier);
90        let (language, text) = self.parse_query(identifier);
91        if text.trim().is_empty() {
92            return LoadResult::Empty {};
93        }
94        let info = self.build_track_info(&language, &text);
95        LoadResult::Track(Track::new(info))
96    }
97    async fn get_track(
98        &self,
99        identifier: &str,
100        routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
101    ) -> Option<BoxedTrack> {
102        let (language, text) = self.parse_query(identifier);
103        let url = self.build_url(&language, &text);
104        Some(Arc::new(HttpTrack {
105            url,
106            local_addr: routeplanner.and_then(|rp| rp.get_address()),
107            proxy: None,
108        }))
109    }
110    fn search_prefixes(&self) -> Vec<&str> {
111        self.search_prefixes.iter().map(|s| s.as_str()).collect()
112    }
113}