1use anyhow::{anyhow, Context, Result};
2use pinyin::ToPinyin;
3use rand::Rng;
4use std::borrow::Cow;
5use std::path::{Path, PathBuf};
6use std::process::Stdio;
7use std::{
8 ffi::OsStr,
9 process::{Child, Command},
10};
11use unicode_segmentation::UnicodeSegmentation;
12
13use crate::config::ServerOverlay;
14
15#[must_use]
16pub fn get_pin_yin(input: &str) -> String {
17 let mut b = String::new();
18 for (index, f) in input.to_pinyin().enumerate() {
19 match f {
20 Some(p) => {
21 b.push_str(p.plain());
22 }
23 None => {
24 if let Some(c) = input.to_uppercase().chars().nth(index) {
25 b.push(c);
26 }
27 }
28 }
29 }
30 b
31}
32
33#[must_use]
35pub fn filetype_supported(current_node: &str) -> bool {
36 let p = Path::new(current_node);
37
38 if p.starts_with("http") {
39 return true;
40 }
41
42 match p.extension() {
43 Some(ext) if ext == "mkv" || ext == "mka" => true,
44 Some(ext) if ext == "mp3" => true,
45 Some(ext) if ext == "aiff" => true,
46 Some(ext) if ext == "flac" => true,
47 Some(ext) if ext == "m4a" => true,
48 Some(ext) if ext == "aac" => true,
49 Some(ext) if ext == "opus" => true,
50 Some(ext) if ext == "ogg" => true,
51 Some(ext) if ext == "wav" => true,
52 Some(ext) if ext == "webm" => true,
53 Some(_) | None => false,
54 }
55}
56
57#[must_use]
58pub fn is_playlist(current_node: &str) -> bool {
59 let p = Path::new(current_node);
60
61 match p.extension() {
62 Some(ext) if ext == "m3u" => true,
63 Some(ext) if ext == "m3u8" => true,
64 Some(ext) if ext == "pls" => true,
65 Some(ext) if ext == "asx" => true,
66 Some(ext) if ext == "xspf" => true,
67 Some(_) | None => false,
68 }
69}
70
71#[must_use]
73pub fn get_parent_folder(path: &Path) -> Cow<'_, Path> {
74 if path.is_dir() {
75 return path.into();
76 }
77 match path.parent() {
78 Some(p) => p.into(),
79 None => std::env::temp_dir().into(),
80 }
81}
82
83pub fn get_app_config_path() -> Result<PathBuf> {
84 let mut path = dirs::config_dir().ok_or_else(|| anyhow!("failed to find os config dir."))?;
85 path.push("termusic");
86
87 if !path.exists() {
88 std::fs::create_dir_all(&path)?;
89 }
90 Ok(path)
91}
92
93fn get_podcast_save_path(config: &ServerOverlay) -> Result<PathBuf> {
95 let full_path = shellexpand::path::tilde(&config.settings.podcast.download_dir);
96 if !full_path.exists() {
97 std::fs::create_dir_all(&full_path)?;
98 }
99 Ok(full_path.into_owned())
100}
101
102pub fn create_podcast_dir(config: &ServerOverlay, pod_title: String) -> Result<PathBuf> {
104 let mut download_path = get_podcast_save_path(config).context("get podcast directory")?;
105 download_path.push(pod_title);
106 std::fs::create_dir_all(&download_path).context("creating podcast download directory")?;
107
108 Ok(download_path)
109}
110
111pub fn playlist_get_vec(current_node: &str) -> Result<Vec<String>> {
113 let playlist_path = Path::new(current_node);
114 let playlist_directory = absolute_path(
116 playlist_path
117 .parent()
118 .ok_or_else(|| anyhow!("cannot get directory from playlist path"))?,
119 )?;
120 let playlist_str = std::fs::read_to_string(playlist_path)?;
121 let items = crate::playlist::decode(&playlist_str)
122 .with_context(|| playlist_path.display().to_string())?;
123 let mut vec = Vec::with_capacity(items.len());
124 for mut item in items {
125 item.absoluteize(&playlist_directory);
126
127 vec.push(item.to_string());
129 }
130 Ok(vec)
131}
132
133#[allow(clippy::module_name_repetitions)]
135pub trait StringUtils {
136 fn substr(&self, start: usize, length: usize) -> &str;
138 fn grapheme_len(&self) -> usize;
140}
141
142impl StringUtils for str {
143 fn substr(&self, start: usize, length: usize) -> &str {
144 if length == 0 {
146 return "";
147 }
148
149 let mut iter = self.grapheme_indices(true).skip(start);
150 let Some((start_idx, _)) = iter.next() else {
152 return "";
153 };
154 match iter.nth(length - 1) {
156 Some((end_idx, _)) => {
157 &self[start_idx..end_idx]
160 }
161 None => {
162 &self[start_idx..]
164 }
165 }
166 }
167
168 fn grapheme_len(&self) -> usize {
169 self.graphemes(true).count()
170 }
171}
172
173impl StringUtils for String {
175 #[inline]
176 fn substr(&self, start: usize, length: usize) -> &str {
177 (**self).substr(start, length)
178 }
179
180 #[inline]
181 fn grapheme_len(&self) -> usize {
182 self.as_str().grapheme_len()
183 }
184}
185
186pub fn spawn_process<A: IntoIterator<Item = S> + Clone, S: AsRef<OsStr>>(
190 prog: &Path,
191 superuser: bool,
192 shout_output: bool,
193 args: A,
194) -> std::io::Result<Child> {
195 let mut cmd = if superuser {
196 let mut cmd_t = Command::new("sudo");
197 cmd_t.arg(prog);
198 cmd_t
199 } else {
200 Command::new(prog)
201 };
202 cmd.stdin(Stdio::null());
203 if !shout_output {
204 cmd.stdout(Stdio::null());
205 cmd.stderr(Stdio::null());
206 }
207
208 cmd.args(args);
209 cmd.spawn()
210}
211
212pub fn absolute_path(path: &Path) -> std::io::Result<Cow<'_, Path>> {
222 if path.is_absolute() {
223 Ok(Cow::Borrowed(path))
224 } else {
225 Ok(Cow::Owned(std::env::current_dir()?.join(path)))
226 }
227}
228
229#[must_use]
241pub fn absolute_path_base<'a>(path: &'a Path, base: &Path) -> Cow<'a, Path> {
242 if path.is_absolute() {
243 Cow::Borrowed(path)
244 } else {
245 Cow::Owned(base.join(path))
246 }
247}
248
249#[must_use]
251pub fn random_ascii(len: usize) -> String {
252 rand::thread_rng()
253 .sample_iter(&rand::distributions::Alphanumeric)
254 .take(len)
255 .map(|v| char::from(v).to_ascii_lowercase())
256 .collect()
257}
258
259pub fn display_with(
279 f: impl Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
280) -> impl std::fmt::Display {
281 struct DisplayWith<F>(F);
282
283 impl<F> std::fmt::Display for DisplayWith<F>
284 where
285 F: Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
286 {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 self.0(f)
289 }
290 }
291
292 DisplayWith(f)
293}
294
295#[cfg(test)]
296mod tests {
297 use std::fmt::{Display, Write};
298
299 use super::*;
300 use pretty_assertions::assert_eq;
301
302 #[test]
303 fn test_pin_yin() {
304 assert_eq!(get_pin_yin("陈一发儿"), "chenyifaer".to_string());
305 assert_eq!(get_pin_yin("Gala乐队"), "GALAledui".to_string());
306 assert_eq!(get_pin_yin("乐队Gala乐队"), "leduiGALAledui".to_string());
307 assert_eq!(get_pin_yin("Annett Louisan"), "ANNETT LOUISAN".to_string());
308 }
309
310 #[test]
311 fn test_substr() {
312 assert_eq!("abcde".substr(0, 0), "");
314
315 assert_eq!("abcde".substr(0, 1), "a");
316 assert_eq!("abcde".substr(4, 1), "e");
317
318 assert_eq!("abcde".substr(100, 1), "");
320 assert_eq!("abcde".substr(3, 3), "de");
322
323 assert_eq!("陈一发儿".substr(0, 1), "陈");
324 assert_eq!("陈一发儿".substr(3, 1), "儿");
325 }
326
327 #[test]
328 fn display_with_to_string() {
329 fn nested() -> impl Display {
330 let new_owned = String::from("Owned");
331
332 display_with(move |f| write!(f, "Nested! {new_owned}"))
333 }
334
335 let mut str = String::new();
336
337 let _ = write!(&mut str, "Formatted! {}", nested());
338
339 assert_eq!(str, "Formatted! Nested! Owned");
340 }
341}