lavende_core/sources/
google_tts.rs1use 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}