lavende_core/sources/
flowery.rs1use crate::{
2 config::sources::FloweryConfig,
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 FlowerySource {
11 config: FloweryConfig,
12 search_prefixes: Vec<String>,
13 url_pattern: Regex,
14}
15impl FlowerySource {
16 pub fn new(config: FloweryConfig) -> Self {
17 Self {
18 config,
19 search_prefixes: vec!["ftts:".to_string()],
20 url_pattern: Regex::new(r"(?i)^ftts://").unwrap(),
21 }
22 }
23 fn build_track_info(&self, text: &str, identifier: &str, url: &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: identifier.to_string(),
31 is_seekable: true,
32 author: "Flowery TTS".to_string(),
33 length: 0,
34 is_stream: false,
35 position: 0,
36 title: title_text,
37 uri: Some(url.to_string()),
38 source_name: self.name().to_string(),
39 artwork_url: None,
40 isrc: None,
41 }
42 }
43 fn build_url(
44 &self,
45 text: &str,
46 params_override: std::collections::HashMap<String, String>,
47 ) -> String {
48 let mut voice = self.config.voice.clone();
49 let mut translate = self.config.translate;
50 let mut silence = self.config.silence;
51 let mut speed = self.config.speed;
52 if !self.config.enforce_config {
53 if let Some(v) = params_override.get("voice") {
54 voice = v.clone();
55 }
56 if let Some(t) = params_override.get("translate") {
57 translate = t.parse().unwrap_or(translate);
58 }
59 if let Some(s) = params_override.get("silence") {
60 silence = s.parse().unwrap_or(silence);
61 }
62 if let Some(sp) = params_override.get("speed") {
63 speed = sp.parse().unwrap_or(speed);
64 }
65 }
66 let encoded_text = urlencoding::encode(text);
67 format!(
68 "https://api.flowery.pw/v1/tts?voice={}&text={}&translate={}&silence={}&audio_format=mp3&speed={}",
69 urlencoding::encode(&voice),
70 encoded_text,
71 translate,
72 silence,
73 speed
74 )
75 }
76 fn parse_query(&self, identifier: &str) -> (String, std::collections::HashMap<String, String>) {
77 let mut path_and_query = identifier;
78 for prefix in &self.search_prefixes {
79 if path_and_query.starts_with(prefix) {
80 path_and_query = path_and_query.trim_start_matches(prefix);
81 break;
82 }
83 }
84 if path_and_query.starts_with("//") {
85 path_and_query = &path_and_query[2..];
86 }
87 let mut params = std::collections::HashMap::new();
88 let text = if let Some(split_idx) = path_and_query.find('?') {
89 let decoded_text = urlencoding::decode(&path_and_query[..split_idx])
90 .unwrap_or_else(|_| std::borrow::Cow::Borrowed(&path_and_query[..split_idx]))
91 .into_owned();
92 let query_str = &path_and_query[split_idx + 1..];
93 for pair in query_str.split('&') {
94 if let Some(eq_idx) = pair.find('=') {
95 let key = &pair[..eq_idx];
96 let value = &pair[eq_idx + 1..];
97 params.insert(
98 urlencoding::decode(key)
99 .unwrap_or(std::borrow::Cow::Borrowed(key))
100 .into_owned(),
101 urlencoding::decode(value)
102 .unwrap_or(std::borrow::Cow::Borrowed(value))
103 .into_owned(),
104 );
105 } else if !pair.is_empty() {
106 params.insert(
107 urlencoding::decode(pair)
108 .unwrap_or(std::borrow::Cow::Borrowed(pair))
109 .into_owned(),
110 "".to_string(),
111 );
112 }
113 }
114 decoded_text
115 } else {
116 urlencoding::decode(path_and_query)
117 .unwrap_or(std::borrow::Cow::Borrowed(path_and_query))
118 .into_owned()
119 };
120 (text, params)
121 }
122}
123#[async_trait]
124impl SourcePlugin for FlowerySource {
125 fn name(&self) -> &str {
126 "flowery"
127 }
128 fn can_handle(&self, identifier: &str) -> bool {
129 self.search_prefixes
130 .iter()
131 .any(|p| identifier.starts_with(p))
132 || self.url_pattern.is_match(identifier)
133 }
134 async fn load(
135 &self,
136 identifier: &str,
137 _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
138 ) -> LoadResult {
139 debug!("Flowery TTS loading: {}", identifier);
140 let (text, params) = self.parse_query(identifier);
141 if text.trim().is_empty() {
142 return LoadResult::Empty {};
143 }
144 let url = self.build_url(&text, params);
145 let info = self.build_track_info(&text, identifier, &url);
146 LoadResult::Track(Track::new(info))
147 }
148 async fn get_track(
149 &self,
150 identifier: &str,
151 routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
152 ) -> Option<BoxedTrack> {
153 let (text, params) = self.parse_query(identifier);
154 let url = self.build_url(&text, params);
155 Some(Arc::new(HttpTrack {
156 url,
157 local_addr: routeplanner.and_then(|rp| rp.get_address()),
158 proxy: None,
159 }))
160 }
161 fn search_prefixes(&self) -> Vec<&str> {
162 self.search_prefixes.iter().map(|s| s.as_str()).collect()
163 }
164}