use crate::{
config::sources::GoogleTtsConfig,
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 GoogleTtsSource {
config: GoogleTtsConfig,
search_prefixes: Vec<String>,
url_pattern: Regex,
}
impl GoogleTtsSource {
pub fn new(config: GoogleTtsConfig) -> Self {
Self {
config,
search_prefixes: vec!["gtts:".to_string(), "speak:".to_string()],
url_pattern: Regex::new(r"(?i)^(gtts://|speak://)").unwrap(),
}
}
fn build_track_info(&self, language: &str, text: &str) -> TrackInfo {
let title_text = if text.len() > 50 {
format!("{}...", &text[..47])
} else {
text.to_string()
};
TrackInfo {
identifier: format!("gtts://{}:{}", language, text),
is_seekable: true,
author: "Google TTS".to_string(),
length: 0,
is_stream: false,
position: 0,
title: format!("TTS: {}", title_text),
uri: Some(self.build_url(language, text)),
source_name: self.name().to_string(),
artwork_url: None,
isrc: None,
}
}
fn build_url(&self, language: &str, text: &str) -> String {
let encoded_text = urlencoding::encode(text);
format!(
"https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl={}&total=1&idx=0&textlen={}&client=gtx",
encoded_text,
language,
text.len()
)
}
fn parse_query(&self, identifier: &str) -> (String, String) {
let mut path = identifier;
for prefix in &self.search_prefixes {
if path.starts_with(prefix) {
path = path.trim_start_matches(prefix);
break;
}
}
if path.starts_with("//") {
path = &path[2..];
}
if let Some(split_idx) = path.find(':') {
let lang = &path[..split_idx];
let actual_text = &path[split_idx + 1..];
if !lang.is_empty() {
return (lang.to_string(), actual_text.to_string());
}
}
(self.config.language.clone(), path.to_string())
}
}
#[async_trait]
impl SourcePlugin for GoogleTtsSource {
fn name(&self) -> &str {
"google-tts"
}
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!("Google TTS loading: {}", identifier);
let (language, text) = self.parse_query(identifier);
if text.trim().is_empty() {
return LoadResult::Empty {};
}
let info = self.build_track_info(&language, &text);
LoadResult::Track(Track::new(info))
}
async fn get_track(
&self,
identifier: &str,
routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
) -> Option<BoxedTrack> {
let (language, text) = self.parse_query(identifier);
let url = self.build_url(&language, &text);
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()
}
}