use std::sync::LazyLock;
use lunar_lib::iterator_ext::IteratorExtensions;
use regex::Regex;
use rgb::Rgb;
use serde::{Deserialize, Serialize};
mod lyric_line;
pub use lyric_line::*;
mod lyric_span;
pub use lyric_span::*;
use crate::lyrics::LyricFormat;
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<LyricLine>,
vocalists: Vec<(String, Option<Rgb<u8>>)>,
tool: Option<String>,
lrc_author: Option<String>,
}
pub struct SyncedLyricsBuilder {
lines: Vec<Option<LyricLine>>,
offset: u32,
vocalists: Vec<(String, Option<Rgb<u8>>)>,
tool: Option<String>,
lrc_author: Option<String>,
}
impl SyncedLyrics {
#[must_use]
pub fn lines(&self) -> &[LyricLine] {
&self.lines
}
#[must_use]
pub fn vocalists(&self) -> &[(String, Option<Rgb<u8>>)] {
&self.vocalists
}
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 RawLyricLine::from_lyric_line(line)? {
RawLyricLine::Line(lyric_line) => builder.add_line(lyric_line),
RawLyricLine::Break => builder.add_break(),
RawLyricLine::Offset(offset) => builder.set_offset(offset),
RawLyricLine::Vocalist(vocalist, color) => builder.add_vocalist(vocalist, color),
RawLyricLine::By(by) => builder.set_lrc_author(by),
RawLyricLine::Tool(tool) => builder.set_tool(tool),
RawLyricLine::Ignored => (),
}
}
Ok(builder.build())
}
#[must_use]
pub fn to_lyrics(&self, format: LyricFormat) -> String {
self.lines
.iter()
.map(|l| l.to_lyrics(format))
.to_vec()
.join("\n")
}
}
impl LyricSpan {
#[must_use]
pub fn new(lyric: &str, start_us: u32) -> Self {
Self {
start_us,
vocals: lyric.to_owned(),
}
}
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)
.try_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();
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(
"Found an invalid span timestamp when parsing a timestamp: '{0}'\nFormat should be '[0,1,2,3]'"
)]
InvalidLineVocalists(String),
#[error("LyricLine timestamp was corrupted: '{0}'")]
CorruptLineTimestamp(String),
#[error("A tag was formatted incorrectly: '{0}'")]
CorruptLineTag(String),
#[error("Found an invalid span timestamp when parsing a LyricSpan: {0}")]
InvalidSpanTimestamp(String),
}
impl Default for SyncedLyricsBuilder {
fn default() -> Self {
Self::new()
}
}
impl SyncedLyricsBuilder {
#[must_use]
pub fn new() -> Self {
Self {
lines: Vec::new(),
offset: 0,
vocalists: Vec::new(),
tool: None,
lrc_author: None,
}
}
pub fn add_line(&mut self, line: LyricLine) {
self.lines.push(Some(line));
}
pub fn add_break(&mut self) {
if self.lines.last().is_some_and(|l| l.is_some()) {
self.lines.push(None);
}
}
pub fn set_offset(&mut self, offset: u32) {
self.offset = offset;
}
#[must_use]
pub fn build(self) -> SyncedLyrics {
let mut iter = self.lines.into_iter().peekable();
let mut lines = Vec::with_capacity(iter.len());
while let Some(item) = iter.next() {
match item {
Some(l) => lines.push(l),
None => {
if let Some(start_us) = iter.peek().and_then(|x| x.as_ref()).map(|l| l.start_us)
{
lines.push(LyricLine {
start_us,
vocals: Vec::new(),
vocalist: None,
});
}
}
}
}
SyncedLyrics {
lines,
vocalists: self.vocalists,
tool: self.tool,
lrc_author: self.lrc_author,
}
}
fn add_vocalist(&mut self, vocalist: String, color: Option<Rgb<u8>>) {
self.vocalists.push((vocalist, color));
}
fn set_lrc_author(&mut self, by: String) {
self.lrc_author = Some(by)
}
fn set_tool(&mut self, tool: String) {
self.tool = Some(tool)
}
}