1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::iter::FusedIterator;
4use std::path::{Path, PathBuf};
5use std::process::Stdio;
6
7use anyhow::{Context, Result, anyhow};
8use pinyin::ToPinyin;
9use rand::Rng;
10use tokio::process::{Child, Command};
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(path: &Path) -> bool {
36 if path.starts_with("http") {
37 return true;
38 }
39
40 let Some(ext) = path.extension().and_then(OsStr::to_str) else {
41 return false;
42 };
43
44 matches!(
45 ext,
46 "mkv"
47 | "mka"
48 | "mp3"
49 | "aiff"
50 | "aif"
51 | "aifc"
52 | "flac"
53 | "m4a"
54 | "aac"
55 | "opus"
56 | "ogg"
57 | "wav"
58 | "webm"
59 )
60}
61
62#[must_use]
64pub fn is_playlist(path: &Path) -> bool {
65 let Some(ext) = path.extension().and_then(OsStr::to_str) else {
66 return false;
67 };
68
69 matches!(ext, "m3u" | "m3u8" | "pls" | "asx" | "xspf")
70}
71
72#[must_use]
74pub fn get_parent_folder(path: &Path) -> Cow<'_, Path> {
75 if path.is_dir() {
76 return path.into();
77 }
78 match path.parent() {
79 Some(p) => p.into(),
80 None => std::env::temp_dir().into(),
81 }
82}
83
84pub fn get_app_config_path() -> Result<PathBuf> {
85 let mut path = dirs::config_dir().ok_or_else(|| anyhow!("failed to find os config dir."))?;
86 path.push("termusic");
87
88 if !path.exists() {
89 std::fs::create_dir_all(&path)?;
90 }
91 Ok(path)
92}
93
94pub fn get_app_new_database_path() -> Result<PathBuf> {
96 let mut db_path = get_app_config_path().context("failed to get app configuration path")?;
97 db_path.push("library2.db");
99
100 Ok(db_path)
101}
102
103fn get_podcast_save_path(config: &ServerOverlay) -> Result<PathBuf> {
105 let full_path = shellexpand::path::tilde(&config.settings.podcast.download_dir);
106 if !full_path.exists() {
107 std::fs::create_dir_all(&full_path)?;
108 }
109 Ok(full_path.into_owned())
110}
111
112pub fn create_podcast_dir(config: &ServerOverlay, pod_title: String) -> Result<PathBuf> {
114 let mut download_path = get_podcast_save_path(config).context("get podcast directory")?;
115 download_path.push(pod_title);
116 std::fs::create_dir_all(&download_path).context("creating podcast download directory")?;
117
118 Ok(download_path)
119}
120
121pub fn playlist_get_vec(playlist_path: &Path) -> Result<Vec<String>> {
123 let playlist_directory = absolute_path(
125 playlist_path
126 .parent()
127 .ok_or_else(|| anyhow!("cannot get directory from playlist path"))?,
128 )?;
129 let playlist_str = std::fs::read_to_string(playlist_path)?;
130 let items = crate::playlist::decode(&playlist_str)
131 .with_context(|| playlist_path.display().to_string())?;
132 let mut vec = Vec::with_capacity(items.len());
133 for mut item in items {
134 item.absoluteize(&playlist_directory);
135
136 vec.push(item.to_string());
138 }
139 Ok(vec)
140}
141
142#[allow(clippy::module_name_repetitions)]
144pub trait StringUtils {
145 fn substr(&self, start: usize, length: usize) -> &str;
147 fn grapheme_len(&self) -> usize;
149}
150
151impl StringUtils for str {
152 fn substr(&self, start: usize, length: usize) -> &str {
153 if length == 0 {
155 return "";
156 }
157
158 let mut iter = self.grapheme_indices(true).skip(start);
159 let Some((start_idx, _)) = iter.next() else {
161 return "";
162 };
163 match iter.nth(length - 1) {
165 Some((end_idx, _)) => {
166 &self[start_idx..end_idx]
169 }
170 None => {
171 &self[start_idx..]
173 }
174 }
175 }
176
177 fn grapheme_len(&self) -> usize {
178 self.graphemes(true).count()
179 }
180}
181
182impl StringUtils for String {
184 #[inline]
185 fn substr(&self, start: usize, length: usize) -> &str {
186 (**self).substr(start, length)
187 }
188
189 #[inline]
190 fn grapheme_len(&self) -> usize {
191 self.as_str().grapheme_len()
192 }
193}
194
195pub fn spawn_process<A: IntoIterator<Item = S> + Clone, S: AsRef<OsStr>>(
199 prog: &Path,
200 superuser: bool,
201 shout_output: bool,
202 args: A,
203) -> std::io::Result<Child> {
204 let mut cmd = if superuser {
205 let mut cmd_t = Command::new("sudo");
206 cmd_t.arg(prog);
207 cmd_t
208 } else {
209 Command::new(prog)
210 };
211 cmd.stdin(Stdio::null());
212 if shout_output {
213 cmd.stdout(Stdio::piped());
214 cmd.stderr(Stdio::piped());
215 } else {
216 cmd.stdout(Stdio::null());
217 cmd.stderr(Stdio::null());
218 }
219
220 cmd.args(args);
221 cmd.spawn()
222}
223
224pub fn absolute_path(path: &Path) -> std::io::Result<Cow<'_, Path>> {
234 if path.is_absolute() {
235 Ok(Cow::Borrowed(path))
236 } else {
237 Ok(Cow::Owned(std::env::current_dir()?.join(path)))
238 }
239}
240
241#[must_use]
253pub fn absolute_path_base<'a>(path: &'a Path, base: &Path) -> Cow<'a, Path> {
254 if path.is_absolute() {
255 Cow::Borrowed(path)
256 } else {
257 Cow::Owned(base.join(path))
258 }
259}
260
261#[must_use]
263pub fn random_ascii(len: usize) -> String {
264 rand::rng()
265 .sample_iter(&rand::distr::Alphanumeric)
266 .take(len)
267 .map(|v| char::from(v).to_ascii_lowercase())
268 .collect()
269}
270
271pub fn display_with(
291 f: impl Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
292) -> impl std::fmt::Display {
293 struct DisplayWith<F>(F);
294
295 impl<F> std::fmt::Display for DisplayWith<F>
296 where
297 F: Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
298 {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 self.0(f)
301 }
302 }
303
304 DisplayWith(f)
305}
306
307#[derive(Debug, Clone)]
315pub struct SplitArrayIter<'a> {
316 val: &'a str,
317 array: &'a [&'a str],
318}
319
320impl<'a> SplitArrayIter<'a> {
321 #[must_use]
322 pub fn new(val: &'a str, array: &'a [&'a str]) -> Self {
323 Self { val, array }
324 }
325}
326
327impl<'a> Iterator for SplitArrayIter<'a> {
328 type Item = &'a str;
329
330 fn next(&mut self) -> Option<Self::Item> {
331 if self.val.is_empty() {
332 return None;
333 }
334
335 let mut found: Option<(&str, &str)> = None;
336
337 for pat in self.array {
344 if let Some((val, remainder)) = self.val.split_once(pat) {
347 if found.is_none_or(|v| v.0.len() > val.len()) {
350 found = Some((val, remainder));
351 }
352 }
353 }
355
356 let (found, remainder) = found.unwrap_or((self.val, ""));
357
358 self.val = remainder;
359
360 Some(found)
361 }
362}
363
364impl FusedIterator for SplitArrayIter<'_> {}
365
366#[cfg(test)]
367mod tests {
368 use std::fmt::{Display, Write};
369
370 use super::*;
371 use pretty_assertions::assert_eq;
372
373 #[test]
374 fn test_pin_yin() {
375 assert_eq!(get_pin_yin("陈一发儿"), "chenyifaer".to_string());
376 assert_eq!(get_pin_yin("Gala乐队"), "GALAledui".to_string());
377 assert_eq!(get_pin_yin("乐队Gala乐队"), "leduiGALAledui".to_string());
378 assert_eq!(get_pin_yin("Annett Louisan"), "ANNETT LOUISAN".to_string());
379 }
380
381 #[test]
382 fn test_substr() {
383 assert_eq!("abcde".substr(0, 0), "");
385
386 assert_eq!("abcde".substr(0, 1), "a");
387 assert_eq!("abcde".substr(4, 1), "e");
388
389 assert_eq!("abcde".substr(100, 1), "");
391 assert_eq!("abcde".substr(3, 3), "de");
393
394 assert_eq!("陈一发儿".substr(0, 1), "陈");
395 assert_eq!("陈一发儿".substr(3, 1), "儿");
396 }
397
398 #[test]
399 fn display_with_to_string() {
400 fn nested() -> impl Display {
401 let new_owned = String::from("Owned");
402
403 display_with(move |f| write!(f, "Nested! {new_owned}"))
404 }
405
406 let mut str = String::new();
407
408 let _ = write!(&mut str, "Formatted! {}", nested());
409
410 assert_eq!(str, "Formatted! Nested! Owned");
411 }
412
413 #[test]
414 fn split_array_single_pattern() {
415 let value = "something++another++test";
416 let pattern = &["++"];
417 let mut iter = SplitArrayIter::new(value, pattern);
418
419 assert_eq!(iter.next(), Some("something"));
420 assert_eq!(iter.next(), Some("another"));
421 assert_eq!(iter.next(), Some("test"));
422 assert_eq!(iter.next(), None);
423 }
424
425 #[test]
426 fn split_array_multi_pattern() {
427 let value = "something++another--test";
428 let pattern = &["++", "--"];
429 let mut iter = SplitArrayIter::new(value, pattern);
430
431 assert_eq!(iter.next(), Some("something"));
432 assert_eq!(iter.next(), Some("another"));
433 assert_eq!(iter.next(), Some("test"));
434 assert_eq!(iter.next(), None);
435 }
436
437 #[test]
438 fn split_array_multi_pattern_interspersed() {
439 let value = "something--test++another--test";
440 let pattern = &["++", "--"];
441 let mut iter = SplitArrayIter::new(value, pattern);
442
443 assert_eq!(iter.next(), Some("something"));
444 assert_eq!(iter.next(), Some("test"));
445 assert_eq!(iter.next(), Some("another"));
446 assert_eq!(iter.next(), Some("test"));
447 assert_eq!(iter.next(), None);
448 }
449
450 #[test]
451 fn split_array_multi_pattern_interspersed2() {
452 let value = "ArtistA, ArtistB feat. ArtistC";
453 let pattern = &["feat.", ","];
455 let mut iter = SplitArrayIter::new(value, pattern).map(str::trim);
456
457 assert_eq!(iter.next(), Some("ArtistA"));
458 assert_eq!(iter.next(), Some("ArtistB"));
459 assert_eq!(iter.next(), Some("ArtistC"));
460 assert_eq!(iter.next(), None);
461
462 let value = "ArtistA, ArtistB feat. ArtistC";
463 let pattern = &[",", "feat."];
465 let mut iter = SplitArrayIter::new(value, pattern).map(str::trim);
466
467 assert_eq!(iter.next(), Some("ArtistA"));
468 assert_eq!(iter.next(), Some("ArtistB"));
469 assert_eq!(iter.next(), Some("ArtistC"));
470 assert_eq!(iter.next(), None);
471 }
472
473 #[test]
474 fn split_array_empty_val() {
475 let mut iter = SplitArrayIter::new("", &["test"]);
476
477 assert_eq!(iter.next(), None);
478 assert_eq!(iter.next(), None);
479 assert_eq!(iter.next(), None);
480 }
481
482 #[test]
483 fn split_array_empty_pat() {
484 let mut iter = SplitArrayIter::new("hello there", &[]);
485
486 assert_eq!(iter.next(), Some("hello there"));
487 assert_eq!(iter.next(), None);
488 assert_eq!(iter.next(), None);
489 }
490}