pub fn format_count(n: u64) -> String {
const UNITS: [(u64, char); 4] = [
(1_000_000_000_000, 'T'),
(1_000_000_000, 'B'),
(1_000_000, 'M'),
(1_000, 'K'),
];
let v = n as f64;
let Some(mut i) = UNITS.iter().position(|&(u, _)| v >= u as f64) else {
return n.to_string();
};
let mut scaled = v / UNITS[i].0 as f64;
if scaled.round() >= 1000.0 {
i = i.saturating_sub(1);
scaled = v / UNITS[i].0 as f64;
}
format!("{scaled:.1}{}", UNITS[i].1)
}
pub fn format_duration(secs: u32) -> std::borrow::Cow<'static, str> {
if secs > 0 {
format!("{}:{:02}", secs / 60, secs % 60).into()
} else {
"--:--".into()
}
}
use std::fmt::Write as _;
pub fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
' ' => out.push('+'),
'+' => out.push_str("%2B"),
'&' => out.push_str("%26"),
'#' => out.push_str("%23"),
'=' => out.push_str("%3D"),
c if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~' => {
out.push(c);
}
c => {
for b in c.to_string().into_bytes() {
let _ = std::write!(out, "%{b:02X}");
}
}
}
}
out
}
pub fn try_probe_duration(path: &str) -> Option<u32> {
use std::fs::File;
use symphonia::core::{
formats::FormatOptions,
io::{MediaSourceStream, MediaSourceStreamOptions},
meta::MetadataOptions,
probe::Hint,
};
let file = File::open(path).ok()?;
let mss = MediaSourceStream::new(Box::new(file), MediaSourceStreamOptions::default());
let mut hint = Hint::new();
if let Some(ext) = std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
{
hint.with_extension(ext);
}
let probed = symphonia::default::get_probe()
.format(
&hint,
mss,
&FormatOptions::default(),
&MetadataOptions::default(),
)
.ok()?;
let track = probed
.format
.tracks()
.iter()
.find(|t| t.codec_params.sample_rate.is_some())?;
let params = &track.codec_params;
let sample_rate = params.sample_rate.filter(|&r| r != 0)?;
let n_frames = params.n_frames?;
Some(n_frames as u32 / sample_rate)
}
pub fn fuzzy_match(query: &str, text: &str) -> bool {
if query.is_empty() {
return true;
}
let query = query.to_lowercase();
let text = text.to_lowercase();
let mut qi = query.chars().peekable();
for c in text.chars() {
if qi.peek() == Some(&c) {
qi.next();
}
}
qi.peek().is_none()
}
pub fn remove_at<T>(list: &mut Vec<T>, indices: &[usize]) -> usize {
let mut sorted: Vec<usize> = indices.to_vec();
sorted.sort_unstable();
sorted.dedup();
let mut removed = 0;
for &i in sorted.iter().rev() {
if i < list.len() {
list.remove(i);
removed += 1;
}
}
removed
}
pub fn reorder_tracks<T: Clone>(
tracks: &mut Vec<T>,
drop_idx: usize,
indices: &[usize],
selection: &[usize],
) -> Vec<usize> {
let sorted_indices: Vec<usize> = {
let mut s = indices.to_vec();
s.sort_unstable();
s
};
let extracted: Vec<T> = sorted_indices
.iter()
.filter_map(|&i| tracks.get(i).cloned())
.collect();
for &i in sorted_indices.iter().rev() {
if i < tracks.len() {
tracks.remove(i);
}
}
let removed_before = sorted_indices.iter().filter(|&&i| i < drop_idx).count();
let adjusted_drop = (drop_idx - removed_before).min(tracks.len());
let new_count = extracted.len();
for (j, track) in extracted.into_iter().enumerate() {
tracks.insert(adjusted_drop + j, track);
}
let mut new_selected: Vec<usize> = Vec::with_capacity(selection.len());
for &sel_idx in selection {
if let Some(pos) = sorted_indices.iter().position(|&i| i == sel_idx) {
new_selected.push(adjusted_drop + pos);
} else {
let removed_before_sel = sorted_indices.iter().filter(|&&i| i < sel_idx).count();
let after_removal = sel_idx - removed_before_sel;
let insert_shift = if after_removal >= adjusted_drop {
new_count
} else {
0
};
new_selected.push(after_removal + insert_shift);
}
}
new_selected.sort_unstable();
new_selected
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Track;
#[test]
fn formats_count_boundaries() {
assert_eq!(format_count(0), "0");
assert_eq!(format_count(999), "999");
assert_eq!(format_count(1_234), "1.2K");
assert_eq!(format_count(9_999), "10.0K");
assert_eq!(format_count(12_345), "12.3K");
assert_eq!(format_count(841_000), "841.0K");
assert_eq!(format_count(999_499), "999.5K");
assert_eq!(format_count(999_500), "1.0M");
assert_eq!(format_count(1_234_567), "1.2M");
assert_eq!(format_count(949_999_999), "950.0M");
assert_eq!(format_count(1_000_000_000), "1.0B");
assert_eq!(format_count(1_234_567_891), "1.2B");
}
#[test]
fn format_duration_seconds() {
assert_eq!(format_duration(0).as_ref(), "--:--");
assert_eq!(format_duration(30).as_ref(), "0:30");
assert_eq!(format_duration(59).as_ref(), "0:59");
}
#[test]
fn format_duration_minutes() {
assert_eq!(format_duration(60).as_ref(), "1:00");
assert_eq!(format_duration(90).as_ref(), "1:30");
assert_eq!(format_duration(369).as_ref(), "6:09");
assert_eq!(format_duration(3600).as_ref(), "60:00");
}
#[test]
fn fuzzy_match_exact() {
assert!(fuzzy_match("hello", "hello"));
}
#[test]
fn fuzzy_match_subsequence() {
assert!(fuzzy_match("hlo", "hello"));
assert!(fuzzy_match("hlo", "Hello World"));
}
#[test]
fn fuzzy_match_empty_query() {
assert!(fuzzy_match("", "anything"));
}
#[test]
fn fuzzy_match_no_match() {
assert!(!fuzzy_match("xyz", "hello"));
}
#[test]
fn fuzzy_match_case_insensitive() {
assert!(fuzzy_match("HELLO", "hello"));
assert!(fuzzy_match("HeLlO", "HELLO"));
}
#[test]
fn fuzzy_match_partial() {
assert!(fuzzy_match("ell", "hello"));
assert!(!fuzzy_match("leh", "hello"));
}
#[test]
fn remove_at_multiple() {
let mut v = vec![0, 1, 2, 3, 4];
assert_eq!(remove_at(&mut v, &[1, 3]), 2);
assert_eq!(v, vec![0, 2, 4]);
}
#[test]
fn remove_at_unsorted_and_dedup() {
let mut v = vec![0, 1, 2, 3, 4];
assert_eq!(remove_at(&mut v, &[3, 0, 3, 99]), 2);
assert_eq!(v, vec![1, 2, 4]);
}
#[test]
fn probe_duration_missing_file() {
assert_eq!(
try_probe_duration("/nonexistent/path/file.mp3").unwrap_or(0),
0
);
}
fn make_tracks(count: usize) -> Vec<Track> {
(0..count)
.map(|i| {
let mut providers = std::collections::HashMap::new();
providers.insert(
crate::providers::ProviderId::YouTube,
crate::types::ProviderTrack {
id: format!("id{i}"),
url: format!("url{i}"),
artist_id: None,
duration: 10,
thumbnail: String::new(),
album: None,
play_count: 0,
},
);
Track {
title: format!("Track {i}"),
artist: "Artist".into(),
source: crate::providers::ProviderId::YouTube,
providers,
}
})
.collect()
}
#[test]
fn move_single_not_selected_remaps_selection() {
let mut tracks = make_tracks(5); let selection = vec![1, 2]; let new_sel = reorder_tracks(&mut tracks, 0, &[4], &selection);
assert_eq!(tracks_ids(&tracks), ["id4", "id0", "id1", "id2", "id3"]);
assert_eq!(new_sel, [2, 3]);
}
#[test]
fn move_single_selected_remaps_selection() {
let mut tracks = make_tracks(5); let selection = vec![1, 2]; let new_sel = reorder_tracks(&mut tracks, 0, &[2], &selection);
assert_eq!(tracks_ids(&tracks), ["id2", "id0", "id1", "id3", "id4"]);
assert_eq!(new_sel, [0, 2]);
}
#[test]
fn move_multiple_selected_remaps_selection() {
let mut tracks = make_tracks(6); let selection = vec![1, 2, 4]; let new_sel = reorder_tracks(&mut tracks, 5, &[1, 2], &selection);
assert_eq!(
tracks_ids(&tracks),
["id0", "id3", "id4", "id1", "id2", "id5"]
);
assert_eq!(new_sel, [2, 3, 4]);
}
#[test]
fn move_non_selected_above_selection() {
let mut tracks = make_tracks(5); let selection = vec![2, 3]; let new_sel = reorder_tracks(&mut tracks, 4, &[0], &selection);
assert_eq!(tracks_ids(&tracks), ["id1", "id2", "id3", "id0", "id4"]);
assert_eq!(new_sel, [1, 2]);
}
#[test]
fn move_non_selected_between_selected() {
let mut tracks = make_tracks(6); let selection = vec![1, 3, 4]; let new_sel = reorder_tracks(&mut tracks, 2, &[0], &selection);
assert_eq!(
tracks_ids(&tracks),
["id1", "id0", "id2", "id3", "id4", "id5"]
);
assert_eq!(new_sel, [0, 3, 4]);
}
#[test]
fn empty_selection_returns_empty() {
let mut tracks = make_tracks(3);
let new_sel = reorder_tracks(&mut tracks, 0, &[1], &[]);
assert!(new_sel.is_empty());
}
#[test]
fn move_all_selected_to_front() {
let mut tracks = make_tracks(5); let selection = vec![0, 1, 2, 3, 4];
let new_sel = reorder_tracks(&mut tracks, 4, &[0, 1, 2, 3, 4], &selection);
assert_eq!(tracks_ids(&tracks), ["id0", "id1", "id2", "id3", "id4"]);
assert_eq!(new_sel, [0, 1, 2, 3, 4]);
}
fn tracks_ids(tracks: &[Track]) -> Vec<&str> {
tracks
.iter()
.map(|t| {
t.provider_id(crate::providers::ProviderId::YouTube)
.unwrap_or("")
})
.collect()
}
#[test]
fn urlencode_keeps_unreserved() {
assert_eq!(urlencode("abc-XYZ_0189.~"), "abc-XYZ_0189.~");
}
#[test]
fn urlencode_space_becomes_plus() {
assert_eq!(urlencode("daft punk"), "daft+punk");
}
#[test]
fn urlencode_reserved_chars() {
assert_eq!(urlencode("a+b&c=d#e"), "a%2Bb%26c%3Dd%23e");
assert_eq!(urlencode("?/:"), "%3F%2F%3A");
assert_eq!(urlencode("%"), "%25");
}
#[test]
fn urlencode_multibyte_utf8() {
assert_eq!(urlencode("café"), "caf%C3%A9");
assert_eq!(
urlencode("初音ミク"),
"%E5%88%9D%E9%9F%B3%E3%83%9F%E3%82%AF"
);
}
}