#[macro_use]
extern crate educe;
mod error;
pub mod tags;
mod timestamp;
use std::{
collections::BTreeSet,
fmt::{self, Display, Formatter, Write},
rc::Rc,
str::FromStr,
};
pub use error::*;
use once_cell::sync::Lazy;
use regex::Regex;
pub use tags::*;
static LYRICS_RE: Lazy<Regex> = Lazy::new(|| Regex::new("^[^\x00-\x08\x0A-\x1F\x7F]*$").unwrap());
static TAG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[.*:.*\]").unwrap());
static LINE_STARTS_WITH_RE: Lazy<Regex> = Lazy::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(())
}
#[derive(Debug, Clone, Educe)]
#[educe(Default(new))]
pub struct Lyrics {
pub metadata: BTreeSet<IDTag>,
timed_lines: Vec<(TimeTag, Rc<str>)>,
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(unsafe { IDTag::from_string_unchecked(label, text) });
},
}
line = line[tag_len..].trim_start();
}
if !has_id_tag || !time_tags.is_empty() {
lyrics.add_line_with_multiple_time_tags(&time_tags, 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)?;
unsafe {
self.add_timed_line_unchecked(time_tag, line.into());
}
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: Rc<str> = line.into();
let len_dec = len - 1;
for time_tag in time_tags.iter().copied().take(len_dec) {
unsafe {
self.add_timed_line_unchecked(time_tag, line.clone());
}
}
unsafe {
self.add_timed_line_unchecked(time_tags[len_dec], line);
}
}
Ok(())
}
#[inline]
unsafe fn add_timed_line_unchecked(&mut self, time_tag: TimeTag, line: Rc<str>) {
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, Rc<str>)] {
&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, Rc<str>) {
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
}
}
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)
}
}