use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
use std::cmp::Ordering;
use std::str::FromStr;
use std::time::Duration;
lazy_static! {
static ref LYRICS_RE: Regex = Regex::new("^[^\x00-\x08\x0A-\x1F\x7F]*$").unwrap();
static ref TAG_RE: Regex = Regex::new(r"\[.*:.*\]").unwrap();
static ref LINE_STARTS_WITH_RE: Regex =
Regex::new("^\\[([^\x00-\x08\x0A-\x1F\x7F\\[\\]:]*):([^\x00-\x08\x0A-\x1F\x7F\\[\\]]*)\\]")
.unwrap();
}
#[derive(Clone)]
pub struct Lyric {
pub offset: i64, pub lang_extension: Option<String>,
pub unsynced_captions: Vec<UnsyncedCaption>, }
#[derive(Clone)]
pub struct UnsyncedCaption {
time_stamp: u64,
text: String,
}
const EOL: &str = "\n";
impl Lyric {
pub fn get_text(&self, mut time: u64) -> Option<String> {
if self.unsynced_captions.is_empty() {
return None;
};
#[allow(clippy::cast_possible_wrap)]
let mut adjusted_time = time as i64 * 1000 + 2000;
adjusted_time += self.offset;
if adjusted_time < 0 {
adjusted_time = 0;
}
time = adjusted_time.abs() as u64;
let mut text = self.unsynced_captions.get(0)?.text.clone();
for v in &self.unsynced_captions {
if time >= v.time_stamp {
text = v.text.clone();
} else {
break;
}
}
Some(text)
}
pub fn get_index(&self, mut time: u64) -> Option<usize> {
if self.unsynced_captions.is_empty() {
return None;
};
#[allow(clippy::cast_possible_wrap)]
let mut adjusted_time = time as i64 * 1000 + 2000;
adjusted_time += self.offset;
if adjusted_time < 0 {
adjusted_time = 0;
}
time = adjusted_time.abs() as u64;
let mut index: usize = 0;
for (i, v) in self.unsynced_captions.iter().enumerate() {
if time >= v.time_stamp {
index = i;
} else {
break;
}
}
Some(index)
}
pub fn adjust_offset(&mut self, time: u64, offset: i64) {
if let Some(index) = self.get_index(time) {
if (index == 0) | (time < 11) {
self.offset -= offset;
} else {
let mut v = &mut self.unsynced_captions[index];
let adjusted_time_stamp = if offset > 0 {
v.time_stamp + offset.abs() as u64
} else {
v.time_stamp - offset.abs() as u64
};
v.time_stamp = match adjusted_time_stamp.cmp(&0) {
Ordering::Greater | Ordering::Equal => adjusted_time_stamp as u64,
Ordering::Less => 0,
};
}
};
self.unsynced_captions
.sort_by(|b, a| b.time_stamp.cmp(&a.time_stamp));
}
pub fn as_lrc_text(&self) -> String {
let mut result: String = String::new();
if self.offset != 0 {
let string_offset = format!("[offset:{}]\n", self.offset);
result += string_offset.as_ref();
}
for line in &self.unsynced_captions {
result += line.as_lrc().as_str();
}
result
}
pub fn merge_adjacent(&mut self) {
let mut unsynced_captions = self.unsynced_captions.clone();
let mut offset = 1;
for (i, v) in self.unsynced_captions.iter().enumerate() {
if i < 1 {
continue;
}
if let Some(item) = unsynced_captions.get(i - offset) {
if v.time_stamp - item.time_stamp < 2000 {
unsynced_captions[i - offset].text += " ";
unsynced_captions[i - offset].text += v.text.as_ref();
unsynced_captions.remove(i - offset + 1);
offset += 1;
}
}
}
self.unsynced_captions = unsynced_captions;
}
}
impl UnsyncedCaption {
fn parse_line(line: &mut String) -> Result<Self, ()> {
let time_stamp = Self::parse_time(
line.get(line.find('[').ok_or(())? + 1..line.find(']').ok_or(())?)
.ok_or(())?,
)?;
let text = line
.drain(line.find(']').ok_or(())? + 1..)
.collect::<String>();
Ok(Self { time_stamp, text })
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn parse_time(string: &str) -> Result<u64, ()> {
if !(string.contains(':')) | !(string.contains('.')) {
return Err(());
}
let (x, y) = (string.find(':').ok_or(())?, string.find('.').ok_or(())?);
let minute = string.get(0..x).ok_or(())?.parse::<u32>().map_err(|_| ())?;
let second = string
.get(x + 1..y)
.ok_or(())?
.parse::<u32>()
.map_err(|_| ())?;
let micros = &format!("0.{}", string.get(y + 1..).ok_or(())?)
.parse::<f64>()
.map_err(|_| ())?;
let sum_milis =
u64::from(minute) * 60 * 1000 + u64::from(second) * 1000 + (micros * 1000.0) as u64;
Ok(sum_milis)
}
fn as_lrc(&self) -> String {
let line = format!("[{}]{}", time_lrc(self.time_stamp), self.text);
line + EOL
}
}
fn time_lrc(time_stamp: u64) -> String {
let time_duration = Duration::from_millis(time_stamp);
let _h = time_duration.as_secs() / 3600;
let m = (time_duration.as_secs() / 60) % 60;
let s = time_duration.as_secs() % 60;
let ms = time_duration.as_millis() % 60;
let res = format!("{:02}:{:02}.{:02}", m, s, ms);
res
}
impl FromStr for Lyric {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut offset: i64 = 0;
let lang_extension = Some(String::new());
let mut unsynced_captions = vec![];
for line in s.split('\n') {
let mut line = line.to_string();
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
let line = line.trim();
let mut line = line.to_string();
if line.is_empty() {
continue;
}
if line.starts_with("[offset") {
let line = line.trim_start_matches("[offset:");
let line = line.trim_end_matches(']');
let line = line.replace(" ", "");
if let Ok(o) = line.parse() {
offset = o;
}
}
if !LINE_STARTS_WITH_RE.is_match(line.as_ref()) {
continue;
}
if let Ok(s) = UnsyncedCaption::parse_line(&mut line) {
unsynced_captions.push(s);
};
}
unsynced_captions.sort_by(|b, a| b.time_stamp.cmp(&a.time_stamp));
let mut lyric = Self {
offset,
lang_extension,
unsynced_captions,
};
lyric.merge_adjacent();
Ok(lyric)
}
}