use anyhow::{bail, Result};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct Song {
pub artist: Option<String>,
pub title: String,
pub mp3: Option<String>,
pub video: Option<String>,
pub edition: Option<String>,
pub genre: Option<String>,
pub year: Option<String>,
pub language: Option<String>,
pub bpm: f32,
pub gap: u32,
pub video_gap: Option<u32>,
pub notes: Vec<Note>,
}
impl TryFrom<String> for Song {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
let lines = value.lines().map(|a| a.trim_start());
let artist = lines
.clone()
.filter_map(|l| l.strip_prefix("#ARTIST:"))
.map(|a| a.to_string())
.next();
let title = lines
.clone()
.filter_map(|l| l.strip_prefix("#TITLE:"))
.map(|a| a.to_string())
.next();
let mp3 = lines
.clone()
.filter_map(|l| l.strip_prefix("#MP3:"))
.map(|a| a.to_string())
.next();
let video = lines
.clone()
.filter_map(|l| l.strip_prefix("#VIDEO:"))
.map(|a| a.to_string())
.next();
let edition = lines
.clone()
.filter_map(|l| l.strip_prefix("#EDITION:"))
.map(|a| a.to_string())
.next();
let genre = lines
.clone()
.filter_map(|l| l.strip_prefix("#GENRE:"))
.map(|a| a.to_string())
.next();
let year = lines
.clone()
.filter_map(|l| l.strip_prefix("#YEAR:"))
.map(|a| a.to_string())
.next();
let language = lines
.clone()
.filter_map(|l| l.strip_prefix("#LANGUAGE:"))
.map(|a| a.to_string())
.next();
let bpm = lines
.clone()
.filter_map(|l| l.strip_prefix("#BPM:"))
.map(|a| a.to_string())
.next();
let gap = lines
.clone()
.filter_map(|l| l.strip_prefix("#GAP:"))
.map(|a| a.to_string())
.next();
let video_gap = lines
.clone()
.filter_map(|l| l.strip_prefix("#VIDEOGAP:"))
.map(|a| a.to_string())
.next();
let notes = lines
.filter(|a| !(a.starts_with('#') || a.starts_with('E') || a.is_empty()))
.filter_map(|a| Note::try_from(a).ok())
.collect::<Vec<_>>();
let title = if let Some(a) = title {
a
} else {
bail!("No title specified!");
};
let bpm = if let Some(a) = bpm {
if let Ok(a) = a.parse::<f32>() {
a
} else {
bail!("BPM specified failed to be parsed!");
}
} else {
bail!("No bpm specified!");
};
let gap = if let Some(a) = gap {
a.parse::<u32>()?
} else {
bail!("No gap specified!");
};
let video_gap = if let Some(a) = video_gap {
Some(a.parse::<u32>()?)
} else {
None
};
Ok(Self {
artist,
title,
mp3,
video,
edition,
genre,
year,
language,
bpm,
gap,
video_gap,
notes,
})
}
}
impl ToString for Song {
fn to_string(&self) -> String {
let mut ret = String::new();
if let Some(artist) = self.artist.as_ref() {
ret.push_str(&format!("#ARTIST:{}\n", artist));
}
ret.push_str(&format!("#TITLE:{}\n", self.title));
if let Some(mp3) = self.mp3.as_ref() {
ret.push_str(&format!("#MP3:{}\n", mp3));
}
if let Some(edition) = self.edition.as_ref() {
ret.push_str(&format!("#EDITION:{}\n", edition));
}
if let Some(genre) = self.genre.as_ref() {
ret.push_str(&format!("#GENRE:{}\n", genre));
}
if let Some(year) = self.year.as_ref() {
ret.push_str(&format!("#YEAR:{}\n", year));
}
if let Some(language) = self.language.as_ref() {
ret.push_str(&format!("#LANGUAGE:{}\n", language));
}
ret.push_str(&format!("#BPM:{}\n", self.bpm));
ret.push_str(&format!("#GAP:{}\n", self.gap));
if let Some(video) = self.video.as_ref() {
ret.push_str(&format!("#VIDEO:{}\n", video));
}
if let Some(video_gap) = self.video_gap.as_ref() {
ret.push_str(&format!("#VIDEOGAP:{}\n", video_gap));
}
for n in self.notes.iter() {
ret.push_str(&n.to_string());
ret.push('\n');
}
ret.push_str("E\n");
ret
}
}
impl Song {
pub fn from_file(path: &str) -> Result<Song> {
let string = std::fs::read_to_string(path)?;
Song::try_from(string)
}
}
impl FromStr for Song {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.to_string().try_into()
}
}
#[derive(Debug, Clone)]
pub struct Note {
pub note_type: NoteType,
pub beat_number: u32,
pub note_length: Option<u32>,
pub note_tone: Option<i32>,
pub lyric: Option<String>,
}
impl TryFrom<&str> for Note {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let mut splot = value.split(' ');
let note_type = splot.next().unwrap().try_into()?;
let beat_number = splot.next().unwrap().parse::<u32>()?;
let (note_length, note_tone, lyric) = if note_type == NoteType::LineBreak {
(None, None, None)
} else {
let note_length = splot.next().unwrap().parse::<u32>()?;
let note_tone = splot.next().unwrap().parse::<i32>()?;
let lyric = splot.collect::<Vec<_>>().join(" ");
(Some(note_length), Some(note_tone), Some(lyric))
};
Ok(Self {
note_type,
beat_number,
note_length,
note_tone,
lyric,
})
}
}
impl ToString for Note {
fn to_string(&self) -> String {
match self.note_type {
NoteType::LineBreak => format!("{} {}", self.note_type.to_string(), self.beat_number),
_ => format!(
"{} {} {} {} {}",
self.note_type.to_string(),
self.beat_number,
self.note_length.unwrap(),
self.note_tone.unwrap(),
self.lyric.as_ref().unwrap()
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoteType {
Normal,
Golden,
Freestyle,
LineBreak,
}
impl TryFrom<&str> for NoteType {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
":" => Self::Normal,
"*" => Self::Golden,
"F" => Self::Freestyle,
"-" => Self::LineBreak,
_ => bail!("Unknown note type: {}", value),
})
}
}
impl ToString for NoteType {
fn to_string(&self) -> String {
match self {
Self::Normal => ":",
Self::Golden => "*",
Self::Freestyle => "F",
Self::LineBreak => "-",
}
.to_string()
}
}
#[test]
pub fn test_manual_serde(){
let text = std::fs::read_to_string("tests/i_hate_everything_about_you.txt").unwrap();
let song = Song::from_str(&text);
assert!(song.is_ok());
let song = song.unwrap();
assert_eq!(text.replace("\r\n", "\n"), song.to_string());
}