#[macro_use]
extern crate educe;
mod error;
pub mod tags;
mod timestamp;
use std::{
collections::BTreeSet,
fmt::{self, Display, Formatter, Write},
str::FromStr,
};
pub use error::*;
use regex::Regex;
use std::sync::LazyLock;
pub use tags::*;
static LYRICS_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new("^[^\x00-\x08\x0A-\x1F\x7F]*$").unwrap());
static TAG_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[.*:.*\]").unwrap());
static LINE_STARTS_WITH_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("^\\[([^\x00-\x08\x0A-\x1F\x7F\\[\\]:]*):([^\x00-\x08\x0A-\x1F\x7F\\[\\]]*)\\]")
.unwrap()
});
fn check_line<S: AsRef<str>>(line: S) -> Result<(), LyricsError> {
let line = line.as_ref();
if !LYRICS_RE.is_match(line) {
return Err(LyricsError::FormatError("Incorrect lyrics."));
}
if TAG_RE.is_match(line) {
return Err(LyricsError::FormatError("Lyrics contain tags."));
}
Ok(())
}
pub trait FromTime {
fn from_timestamp(ms: i64) -> Self;
fn none() -> Self;
}
impl FromTime for u32 {
fn from_timestamp(ms: i64) -> Self {
ms as u32
}
fn none() -> Self {
0
}
}
impl FromTime for Option<u32> {
fn from_timestamp(ms: i64) -> Self {
Some(ms as u32)
}
fn none() -> Self {
None
}
}
impl FromTime for u64 {
fn from_timestamp(ms: i64) -> Self {
ms as u64
}
fn none() -> Self {
0
}
}
impl FromTime for Option<u64> {
fn from_timestamp(ms: i64) -> Self {
Some(ms as u64)
}
fn none() -> Self {
None
}
}
impl FromTime for f64 {
fn from_timestamp(ms: i64) -> Self {
ms as f64 / 1000.0
}
fn none() -> Self {
0.0
}
}
impl FromTime for Option<f64> {
fn from_timestamp(ms: i64) -> Self {
Some(ms as f64 / 1000.0)
}
fn none() -> Self {
None
}
}
#[derive(Debug, Clone, Educe)]
#[educe(Default(new))]
pub struct Lyrics {
pub metadata: BTreeSet<IDTag>,
timed_lines: Vec<(TimeTag, String)>,
lines: Vec<String>,
}
impl Lyrics {
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: AsRef<str>>(s: S) -> Result<Lyrics, LyricsError> {
let mut lyrics: Lyrics = Lyrics::new();
let s = s.as_ref();
let lines: Vec<&str> = s.split('\n').collect();
for line in lines {
let mut time_tags: Vec<TimeTag> = Vec::new();
let mut has_id_tag = false;
let mut line = line.trim();
while let Some(c) = LINE_STARTS_WITH_RE.captures(line) {
let tag = c.get(0).unwrap().as_str();
let tag_len = tag.len();
match TimeTag::from_str(tag) {
Ok(time_tag) => {
time_tags.push(time_tag);
}
Err(_) => {
let label = c.get(1).unwrap().as_str().trim();
if label.is_empty() {
line = "";
break;
}
let text = c.get(2).unwrap().as_str().trim();
has_id_tag = true;
lyrics
.metadata
.insert(IDTag::from_string_unchecked(label, text));
}
}
line = line[tag_len..].trim_start();
}
if !has_id_tag || !time_tags.is_empty() {
let mut clean_line = String::with_capacity(line.len());
let mut inside_angle = false;
for ch in line.chars() {
match ch {
'<' => inside_angle = true,
'>' => inside_angle = false,
_ if !inside_angle => clean_line.push(ch),
_ => {}
}
}
clean_line = clean_line.split_whitespace().collect::<Vec<_>>().join(" ");
lyrics.add_line_with_multiple_time_tags(&time_tags, clean_line)?;
}
}
Ok(lyrics)
}
}
impl Lyrics {
#[inline]
pub fn add_line<S: Into<String>>(&mut self, line: S) -> Result<(), LyricsError> {
let line = line.into();
check_line(&line)?;
self.lines.push(line);
Ok(())
}
#[inline]
pub fn add_timed_line<S: Into<String>>(
&mut self,
time_tag: TimeTag,
line: S,
) -> Result<(), LyricsError> {
let line = line.into();
check_line(&line)?;
self.add_timed_line_unchecked(time_tag, line);
Ok(())
}
pub fn add_line_with_multiple_time_tags<S: Into<String>>(
&mut self,
time_tags: &[TimeTag],
line: S,
) -> Result<(), LyricsError> {
let line = line.into();
check_line(&line)?;
let len = time_tags.len();
if len == 0 {
self.lines.push(line);
} else {
let line: String = line;
let len_dec = len - 1;
for time_tag in time_tags.iter().copied().take(len_dec) {
self.add_timed_line_unchecked(time_tag, line.clone());
}
self.add_timed_line_unchecked(time_tags[len_dec], line);
}
Ok(())
}
#[inline]
fn add_timed_line_unchecked(&mut self, time_tag: TimeTag, line: String) {
let mut insert_index = self.timed_lines.len();
while insert_index > 0 {
insert_index -= 1;
let temp = &self.timed_lines[insert_index].0;
if temp <= &time_tag {
insert_index += 1;
break;
}
}
self.timed_lines.insert(insert_index, (time_tag, line));
}
}
impl Lyrics {
#[inline]
pub fn get_lines(&self) -> &[String] {
&self.lines
}
#[inline]
pub fn get_timed_lines(&self) -> &[(TimeTag, String)] {
&self.timed_lines
}
#[inline]
pub fn remove_line(&mut self, index: usize) -> String {
self.lines.remove(index)
}
#[inline]
pub fn remove_timed_line(&mut self, index: usize) -> (TimeTag, String) {
self.timed_lines.remove(index)
}
#[inline]
pub fn find_timed_line_index<N: Into<i64>>(&self, timestamp: N) -> Option<usize> {
let target_time_tag = TimeTag::new(timestamp);
for (i, (time_tag, _)) in self.timed_lines.iter().enumerate().rev() {
if target_time_tag >= *time_tag {
return Some(i);
}
}
None
}
#[inline]
pub fn to_vec<T: FromTime>(&self) -> Vec<(T, String)> {
let timed_lines = self.get_timed_lines();
if !timed_lines.is_empty() {
timed_lines
.iter()
.map(|(time, text)| (T::from_timestamp(time.get_timestamp()), text.clone()))
.collect()
} else {
self.get_lines()
.iter()
.filter(|line| !line.is_empty())
.map(|line| (T::none(), line.clone()))
.collect()
}
}
}
impl Display for Lyrics {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
let metadata_not_empty = !self.metadata.is_empty();
let timed_lines_not_empty = !self.timed_lines.is_empty();
let lines_not_empty = !self.lines.is_empty();
if metadata_not_empty {
let mut iter = self.metadata.iter();
Display::fmt(iter.next().unwrap(), f)?;
for id_tag in iter {
f.write_char('\n')?;
Display::fmt(id_tag, f)?;
}
}
if timed_lines_not_empty {
if metadata_not_empty {
f.write_char('\n')?;
f.write_char('\n')?;
}
let mut iter = self.timed_lines.iter();
let (time_tag, line) = iter.next().unwrap();
Display::fmt(time_tag, f)?;
f.write_str(line)?;
for (time_tag, line) in iter {
f.write_char('\n')?;
Display::fmt(time_tag, f)?;
f.write_str(line)?;
}
}
if lines_not_empty {
let mut buffer = String::new();
let mut iter = self.lines.iter();
buffer.push_str(iter.next().unwrap());
for line in iter {
buffer.push('\n');
buffer.push_str(line);
}
let s = buffer.trim();
if !s.is_empty() {
if metadata_not_empty || timed_lines_not_empty {
f.write_char('\n')?;
f.write_char('\n')?;
}
f.write_str(s)?;
}
}
Ok(())
}
}
impl FromStr for Lyrics {
type Err = LyricsError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Lyrics::from_str(s)
}
}