drift/src/model.rs

137 lines
3.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Track {
pub id: String,
pub title: String,
pub artist: String,
#[serde(default)]
pub album: Option<String>,
#[serde(default)]
pub duration: Option<u32>,
}
impl Track {
pub fn url(&self) -> String {
format!("https://music.youtube.com/watch?v={}", self.id)
}
pub fn label(&self) -> String {
format!("{} — {}", self.title, self.artist)
}
}
pub fn split_artists(credit: &str) -> Vec<String> {
const MARK: char = '\u{1}';
let mut s = credit.to_string();
for sep in [
" & ", ", ", " feat. ", " Feat. ", " feat ", " ft. ", " Ft. ", " × ",
] {
s = s.replace(sep, &MARK.to_string());
}
let mut out: Vec<String> = Vec::new();
for part in s.split(MARK) {
let name = part.trim();
if !name.is_empty() && !out.iter().any(|o| o == name) {
out.push(name.to_string());
}
}
out
}
pub fn is_valid_id(id: &str) -> bool {
id.len() == 11
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}
pub fn parse_clock(s: &str) -> Option<f64> {
let s = s.trim().trim_end_matches('s');
if s.is_empty() {
return None;
}
let mut total = 0.0;
for part in s.split(':') {
let v: f64 = part.trim().parse().ok()?;
if !v.is_finite() || v < 0.0 {
return None;
}
total = total * 60.0 + v;
}
Some(total)
}
pub fn fmt_time(secs: f64) -> String {
let s = if secs.is_finite() {
secs.max(0.0) as u64
} else {
0
};
let (h, m, s) = (s / 3600, (s % 3600) / 60, s % 60);
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m}:{s:02}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RepeatMode {
Off,
All,
One,
}
impl RepeatMode {
pub fn cycle(self) -> Self {
match self {
RepeatMode::Off => RepeatMode::All,
RepeatMode::All => RepeatMode::One,
RepeatMode::One => RepeatMode::Off,
}
}
pub fn label(self) -> &'static str {
match self {
RepeatMode::Off => "off",
RepeatMode::All => "all",
RepeatMode::One => "one",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clock_parsing() {
assert_eq!(parse_clock("3:45"), Some(225.0));
assert_eq!(parse_clock("1:02:03"), Some(3723.0));
assert_eq!(parse_clock("90"), Some(90.0));
assert_eq!(parse_clock("abc"), None);
assert_eq!(parse_clock("NaN"), None);
assert_eq!(parse_clock("inf"), None);
assert_eq!(fmt_time(225.0), "3:45");
assert_eq!(fmt_time(3723.0), "1:02:03");
}
#[test]
fn artist_splitting() {
assert_eq!(split_artists("Daft Punk"), ["Daft Punk"]);
assert_eq!(split_artists("A & B"), ["A", "B"]);
assert_eq!(split_artists("A, B & C"), ["A", "B", "C"]);
assert_eq!(split_artists("A feat. B"), ["A", "B"]);
assert_eq!(split_artists("A, A"), ["A"]);
assert!(split_artists(" ").is_empty());
}
#[test]
fn ids() {
assert!(is_valid_id("dQw4w9WgXcQ"));
assert!(!is_valid_id("short"));
assert!(!is_valid_id("dQw4w9WgXc&"));
}
}