Skip to main content

lavende_core/sources/
flowery.rs

1use 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}