1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::iter::FusedIterator;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow};
7use pinyin::ToPinyin;
8use rand::Rng;
9use unicode_segmentation::UnicodeSegmentation;
10
11use crate::config::ServerOverlay;
12
13#[must_use]
14pub fn get_pin_yin(input: &str) -> String {
15 let mut b = String::new();
16 for (index, f) in input.to_pinyin().enumerate() {
17 match f {
18 Some(p) => {
19 b.push_str(p.plain());
20 }
21 None => {
22 if let Some(c) = input.to_uppercase().chars().nth(index) {
23 b.push(c);
24 }
25 }
26 }
27 }
28 b
29}
30
31#[must_use]
33pub fn filetype_supported(path: &Path) -> bool {
34 if path.starts_with("http") {
35 return true;
36 }
37
38 let Some(ext) = path.extension().and_then(OsStr::to_str) else {
39 return false;
40 };
41
42 matches!(
43 ext,
44 "mkv"
45 | "mka"
46 | "mp3"
47 | "aiff"
48 | "aif"
49 | "aifc"
50 | "flac"
51 | "m4a"
52 | "aac"
53 | "opus"
54 | "ogg"
55 | "wav"
56 | "webm"
57 )
58}
59
60#[must_use]
62pub fn is_playlist(path: &Path) -> bool {
63 let Some(ext) = path.extension().and_then(OsStr::to_str) else {
64 return false;
65 };
66
67 matches!(ext, "m3u" | "m3u8" | "pls" | "asx" | "xspf")
68}
69
70#[must_use]
72pub fn get_parent_folder(path: &Path) -> Cow<'_, Path> {
73 if path.is_dir() {
74 return path.into();
75 }
76 match path.parent() {
77 Some(p) => p.into(),
78 None => std::env::temp_dir().into(),
79 }
80}
81
82pub fn get_app_config_path() -> Result<PathBuf> {
83 let mut path = dirs::config_dir().ok_or_else(|| anyhow!("failed to find os config dir."))?;
84 path.push("termusic");
85
86 if !path.exists() {
87 std::fs::create_dir_all(&path)?;
88 }
89 Ok(path)
90}
91
92pub fn get_app_new_database_path() -> Result<PathBuf> {
94 let mut db_path = get_app_config_path().context("failed to get app configuration path")?;
95 db_path.push("library2.db");
97
98 Ok(db_path)
99}
100
101fn get_podcast_save_path(config: &ServerOverlay) -> Result<PathBuf> {
103 let full_path = shellexpand::path::tilde(&config.settings.podcast.download_dir);
104 if !full_path.exists() {
105 std::fs::create_dir_all(&full_path)?;
106 }
107 Ok(full_path.into_owned())
108}
109
110pub fn create_podcast_dir(config: &ServerOverlay, pod_title: String) -> Result<PathBuf> {
112 let mut download_path = get_podcast_save_path(config).context("get podcast directory")?;
113 download_path.push(pod_title);
114 std::fs::create_dir_all(&download_path).context("creating podcast download directory")?;
115
116 Ok(download_path)
117}
118
119pub fn playlist_get_vec(playlist_path: &Path) -> Result<Vec<String>> {
121 let playlist_directory = absolute_path(
123 playlist_path
124 .parent()
125 .ok_or_else(|| anyhow!("cannot get directory from playlist path"))?,
126 )?;
127 let playlist_str = std::fs::read_to_string(playlist_path)?;
128 let items = crate::playlist::decode(&playlist_str)
129 .with_context(|| playlist_path.display().to_string())?;
130 let mut vec = Vec::with_capacity(items.len());
131 for mut item in items {
132 item.absoluteize(&playlist_directory);
133
134 vec.push(item.to_string());
136 }
137 Ok(vec)
138}
139
140#[allow(clippy::module_name_repetitions)]
142pub trait StringUtils {
143 fn substr(&self, start: usize, length: usize) -> &str;
145 fn grapheme_len(&self) -> usize;
147}
148
149impl StringUtils for str {
150 fn substr(&self, start: usize, length: usize) -> &str {
151 if length == 0 {
153 return "";
154 }
155
156 let mut iter = self.grapheme_indices(true).skip(start);
157 let Some((start_idx, _)) = iter.next() else {
159 return "";
160 };
161 match iter.nth(length - 1) {
163 Some((end_idx, _)) => {
164 &self[start_idx..end_idx]
167 }
168 None => {
169 &self[start_idx..]
171 }
172 }
173 }
174
175 fn grapheme_len(&self) -> usize {
176 self.graphemes(true).count()
177 }
178}
179
180impl StringUtils for String {
182 #[inline]
183 fn substr(&self, start: usize, length: usize) -> &str {
184 (**self).substr(start, length)
185 }
186
187 #[inline]
188 fn grapheme_len(&self) -> usize {
189 self.as_str().grapheme_len()
190 }
191}
192
193pub fn absolute_path(path: &Path) -> std::io::Result<Cow<'_, Path>> {
203 if path.is_absolute() {
204 Ok(Cow::Borrowed(path))
205 } else {
206 Ok(Cow::Owned(std::env::current_dir()?.join(path)))
207 }
208}
209
210#[must_use]
222pub fn absolute_path_base<'a>(path: &'a Path, base: &Path) -> Cow<'a, Path> {
223 if path.is_absolute() {
224 Cow::Borrowed(path)
225 } else {
226 Cow::Owned(base.join(path))
227 }
228}
229
230#[must_use]
232pub fn random_ascii(len: usize) -> String {
233 rand::rng()
234 .sample_iter(&rand::distr::Alphanumeric)
235 .take(len)
236 .map(|v| char::from(v).to_ascii_lowercase())
237 .collect()
238}
239
240pub fn display_with(
260 f: impl Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
261) -> impl std::fmt::Display {
262 struct DisplayWith<F>(F);
263
264 impl<F> std::fmt::Display for DisplayWith<F>
265 where
266 F: Fn(&mut std::fmt::Formatter<'_>) -> std::fmt::Result,
267 {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 self.0(f)
270 }
271 }
272
273 DisplayWith(f)
274}
275
276#[derive(Debug, Clone)]
284pub struct SplitArrayIter<'a> {
285 val: &'a str,
286 array: &'a [&'a str],
287}
288
289impl<'a> SplitArrayIter<'a> {
290 #[must_use]
291 pub fn new(val: &'a str, array: &'a [&'a str]) -> Self {
292 Self { val, array }
293 }
294}
295
296impl<'a> Iterator for SplitArrayIter<'a> {
297 type Item = &'a str;
298
299 fn next(&mut self) -> Option<Self::Item> {
300 if self.val.is_empty() {
301 return None;
302 }
303
304 let mut found: Option<(&str, &str)> = None;
305
306 for pat in self.array {
313 if let Some((val, remainder)) = self.val.split_once(pat) {
316 if found.is_none_or(|v| v.0.len() > val.len()) {
319 found = Some((val, remainder));
320 }
321 }
322 }
324
325 let (found, remainder) = found.unwrap_or((self.val, ""));
326
327 self.val = remainder;
328
329 Some(found)
330 }
331}
332
333impl FusedIterator for SplitArrayIter<'_> {}
334
335#[cfg(test)]
336mod tests {
337 use std::fmt::{Display, Write};
338
339 use super::*;
340 use pretty_assertions::assert_eq;
341
342 #[test]
343 fn test_pin_yin() {
344 assert_eq!(get_pin_yin("陈一发儿"), "chenyifaer".to_string());
345 assert_eq!(get_pin_yin("Gala乐队"), "GALAledui".to_string());
346 assert_eq!(get_pin_yin("乐队Gala乐队"), "leduiGALAledui".to_string());
347 assert_eq!(get_pin_yin("Annett Louisan"), "ANNETT LOUISAN".to_string());
348 }
349
350 #[test]
351 fn test_substr() {
352 assert_eq!("abcde".substr(0, 0), "");
354
355 assert_eq!("abcde".substr(0, 1), "a");
356 assert_eq!("abcde".substr(4, 1), "e");
357
358 assert_eq!("abcde".substr(100, 1), "");
360 assert_eq!("abcde".substr(3, 3), "de");
362
363 assert_eq!("陈一发儿".substr(0, 1), "陈");
364 assert_eq!("陈一发儿".substr(3, 1), "儿");
365 }
366
367 #[test]
368 fn display_with_to_string() {
369 fn nested() -> impl Display {
370 let new_owned = String::from("Owned");
371
372 display_with(move |f| write!(f, "Nested! {new_owned}"))
373 }
374
375 let mut str = String::new();
376
377 let _ = write!(&mut str, "Formatted! {}", nested());
378
379 assert_eq!(str, "Formatted! Nested! Owned");
380 }
381
382 #[test]
383 fn split_array_single_pattern() {
384 let value = "something++another++test";
385 let pattern = &["++"];
386 let mut iter = SplitArrayIter::new(value, pattern);
387
388 assert_eq!(iter.next(), Some("something"));
389 assert_eq!(iter.next(), Some("another"));
390 assert_eq!(iter.next(), Some("test"));
391 assert_eq!(iter.next(), None);
392 }
393
394 #[test]
395 fn split_array_multi_pattern() {
396 let value = "something++another--test";
397 let pattern = &["++", "--"];
398 let mut iter = SplitArrayIter::new(value, pattern);
399
400 assert_eq!(iter.next(), Some("something"));
401 assert_eq!(iter.next(), Some("another"));
402 assert_eq!(iter.next(), Some("test"));
403 assert_eq!(iter.next(), None);
404 }
405
406 #[test]
407 fn split_array_multi_pattern_interspersed() {
408 let value = "something--test++another--test";
409 let pattern = &["++", "--"];
410 let mut iter = SplitArrayIter::new(value, pattern);
411
412 assert_eq!(iter.next(), Some("something"));
413 assert_eq!(iter.next(), Some("test"));
414 assert_eq!(iter.next(), Some("another"));
415 assert_eq!(iter.next(), Some("test"));
416 assert_eq!(iter.next(), None);
417 }
418
419 #[test]
420 fn split_array_multi_pattern_interspersed2() {
421 let value = "ArtistA, ArtistB feat. ArtistC";
422 let pattern = &["feat.", ","];
424 let mut iter = SplitArrayIter::new(value, pattern).map(str::trim);
425
426 assert_eq!(iter.next(), Some("ArtistA"));
427 assert_eq!(iter.next(), Some("ArtistB"));
428 assert_eq!(iter.next(), Some("ArtistC"));
429 assert_eq!(iter.next(), None);
430
431 let value = "ArtistA, ArtistB feat. ArtistC";
432 let pattern = &[",", "feat."];
434 let mut iter = SplitArrayIter::new(value, pattern).map(str::trim);
435
436 assert_eq!(iter.next(), Some("ArtistA"));
437 assert_eq!(iter.next(), Some("ArtistB"));
438 assert_eq!(iter.next(), Some("ArtistC"));
439 assert_eq!(iter.next(), None);
440 }
441
442 #[test]
443 fn split_array_empty_val() {
444 let mut iter = SplitArrayIter::new("", &["test"]);
445
446 assert_eq!(iter.next(), None);
447 assert_eq!(iter.next(), None);
448 assert_eq!(iter.next(), None);
449 }
450
451 #[test]
452 fn split_array_empty_pat() {
453 let mut iter = SplitArrayIter::new("hello there", &[]);
454
455 assert_eq!(iter.next(), Some("hello there"));
456 assert_eq!(iter.next(), None);
457 assert_eq!(iter.next(), None);
458 }
459}