use std::{fmt::Write, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
mod lyric_line;
pub use lyric_line::*;
mod lyric_span;
pub use lyric_span::*;
static SPAN_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"<(\d+):(\d+\.\d+)>([^<]*)").unwrap());
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SyncedLyrics {
lines: Vec<LyricLineType>,
}
pub struct SyncedLyricsBuilder {
lines: Vec<LyricLineType>,
offset: u32,
}
impl SyncedLyrics {
pub fn lines(&self) -> &[LyricLineType] {
&self.lines
}
pub fn lyric_lines(&self) -> Vec<&LyricLine> {
self.lines
.iter()
.filter_map(|l| match l {
LyricLineType::Lyric(l) => Some(l),
LyricLineType::Break => None,
})
.collect()
}
pub fn from_synced_lyrics(string: impl AsRef<str>) -> Result<Self, LyricParseError> {
let lines = string.as_ref().lines();
let mut builder = SyncedLyricsBuilder::new();
for line in lines {
match LyricLineType::from_lyric_line(line)? {
LyricLineParseResult::Line(lyric_line) => builder.add_line(lyric_line),
LyricLineParseResult::Break => builder.add_break(),
LyricLineParseResult::Offset(offset) => builder.set_offset(offset),
LyricLineParseResult::Ignored => (),
}
}
Ok(builder.build())
}
pub fn to_lrc_string(&self) -> String {
let mut buf = String::new();
for line in &self.lines {
match line {
LyricLineType::Break => buf.write_char('\n').unwrap(),
LyricLineType::Lyric(lyric_line) => {
writeln!(buf, "{}", lyric_line.to_lrc_string()).unwrap()
}
}
}
buf
}
pub fn to_plain_lyrics(&self) -> String {
let mut buf = String::new();
for line in &self.lines {
match line {
LyricLineType::Break => buf.write_char('\n').unwrap(),
LyricLineType::Lyric(lyric_line) => {
writeln!(buf, "{}", lyric_line.to_plain_string()).unwrap()
}
}
}
buf.trim().to_owned()
}
}
impl LyricSpan {
pub fn new(lyric: &str, start_us: u32) -> Self {
Self {
start_us,
vocals: lyric.to_owned(),
vocalists: vec![0],
}
}
pub fn from_lyric_str(lyric: &str, start_us: u32) -> Result<Vec<Self>, LyricParseError> {
let first_span_end = lyric.find('<').unwrap_or(lyric.len());
let initial_text = &lyric[..first_span_end];
let mut spans: Vec<Self> = SPAN_REGEX
.captures_iter(lyric)
.map(|cap| {
let mins = &cap[1];
let secs = &cap[2];
let mins: u32 = mins
.parse()
.map_err(|_| LyricParseError::InvalidSpanTimestamp(format!("{mins}:{secs}")))?;
let secs: f64 = secs
.parse()
.map_err(|_| LyricParseError::InvalidSpanTimestamp(format!("{mins}:{secs}")))?;
let start_us = mins * 60_000_000 + (secs * 1_000_000.0) as u32;
let lyric = &cap[3];
Ok(Self::new(lyric, start_us))
})
.collect::<Result<Vec<_>, LyricParseError>>()?;
if spans.is_empty() {
Ok(vec![Self::new(lyric, start_us)])
} else {
if !initial_text.is_empty() {
spans.insert(0, Self::new(initial_text, start_us));
}
Ok(spans)
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LyricParseError {
#[error("Found an invalid span timestamp when parsing a LyricLine: {0}")]
InvalidLineTimestamp(String),
#[error("LyricLine timestamp was corrupted")]
CorruptLineTag,
#[error("Found an invalid span timestamp when parsing a LyricSpan: {0}")]
InvalidSpanTimestamp(String),
}
impl Default for SyncedLyricsBuilder {
fn default() -> Self {
Self::new()
}
}
impl SyncedLyricsBuilder {
pub fn new() -> Self {
Self {
lines: Vec::new(),
offset: 0,
}
}
pub fn add_line(&mut self, line: LyricLine) {
self.lines.push(LyricLineType::Lyric(line));
}
pub fn add_break(&mut self) {
if self.lines.last().is_none_or(|l| *l == LyricLineType::Break) {
return;
}
self.lines.push(LyricLineType::Break);
}
pub fn set_offset(&mut self, offset: u32) {
self.offset = offset
}
pub fn build(mut self) -> SyncedLyrics {
for line in &mut self.lines {
match line {
LyricLineType::Break => (),
LyricLineType::Lyric(lyric_line) => lyric_line.start_us += self.offset,
}
}
SyncedLyrics { lines: self.lines }
}
}