uta 0.1.2

Command-line music search and downloader for QQ Music and NetEase Cloud Music, lossless first, shipped as a single static binary. For learning and research only; non-commercial use.
//! LRC 歌词的通用处理(与音源无关)。

/// 移植自 Python `cleanlrc`:统一换行、去掉每行首尾空白与 BOM/零宽字符,
/// 丢弃空行和只有时间戳的行。
pub fn clean(text: &str) -> String {
    const STRIP: &[char] = &[
        '\u{feff}', '\u{200b}', '\u{200c}', '\u{200d}', '\u{2060}', '\u{a0}', ' ', '\t',
    ];
    text.replace("\r\n", "\n")
        .replace('\r', "\n")
        .split('\n')
        .map(|l| l.trim_matches(STRIP).trim())
        .filter(|l| !l.is_empty() && !is_bare_timestamp(l))
        .collect::<Vec<_>>()
        .join("\n")
}

/// 匹配 `[mm:ss]`、`[mm:ss.xx]`、`[hh:mm:ss.xxx]` 这类整行只有时间戳的行。
fn is_bare_timestamp(line: &str) -> bool {
    let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) else {
        return false;
    };
    let (main, frac) = match inner.split_once('.') {
        Some((m, f)) => (m, Some(f)),
        None => (inner, None),
    };
    if let Some(f) = frac
        && (!(1..=3).contains(&f.len()) || !f.bytes().all(|b| b.is_ascii_digit()))
    {
        return false;
    }
    let parts: Vec<&str> = main.split(':').collect();
    (2..=3).contains(&parts.len())
        && parts
            .iter()
            .all(|p| p.len() == 2 && p.bytes().all(|b| b.is_ascii_digit()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clean_rules() {
        let raw = "\u{feff}[ti:夜曲]\r\n\r\n  [00:04.99]词:方文山  \r[00:10.00]\n[01:02:03.5]\n\t\n[00:12.81]一闪一闪";
        assert_eq!(
            clean(raw),
            "[ti:夜曲]\n[00:04.99]词:方文山\n[00:12.81]一闪一闪"
        );
    }

    #[test]
    fn bare_timestamps() {
        assert!(is_bare_timestamp("[00:10]"));
        assert!(is_bare_timestamp("[00:10.1]"));
        assert!(is_bare_timestamp("[00:10.123]"));
        assert!(is_bare_timestamp("[01:00:10.12]"));
        assert!(!is_bare_timestamp("[00:10.1234]"));
        assert!(!is_bare_timestamp("[ti:x]"));
        assert!(!is_bare_timestamp("[00:10]歌词"));
        assert!(!is_bare_timestamp("[0:10]"));
    }
}