ultrastar_txt/
parser.rs

1use crate::structs::{Header, Line, Note};
2use regex::Regex;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6error_chain! {
7    errors {
8        #[doc="duplicate header tag was found"]
9        DuplicateHeader(line: u32, tag: &'static str) {
10            description("duplicate header")
11            display("additional {} tag found in line: {}", line, tag)
12        }
13        #[doc="an essential header is missing"]
14        MissingEssential {
15            description("essential header is missing")
16        }
17
18        #[doc="value could not be parsed"]
19        ValueError(line: u32, field: &'static str) {
20            description("could not parse value")
21            display("could not parse {} in line: {}", line, field)
22        }
23        #[doc="an unknown note type was found"]
24        UnknownNoteType(line: u32) {
25            description("unknown note type")
26            display("unknown note type in line: {}", line)
27        }
28        #[doc="could not parse the line at all"]
29        ParserFailure(line: u32) {
30            description("could not parse line")
31            display("could not parse line: {}", line)
32        }
33        #[doc="song is missing the end terminator"]
34        MissingEndIndicator {
35            description("missing end indicator")
36        }
37        #[doc="song file uses a feature that is not implemented"]
38        NotImplemented(line: u32, feature: &'static str) {
39            description("not implemented")
40            display("the feature {} in line {} is not implemented", line, feature)
41        }
42    }
43}
44
45/// Parses the Header of a given Ultrastar Song and returns a Header struct
46///
47/// # Arguments
48/// * txt_str  - a &str that contains the song to parse
49///
50pub fn parse_txt_header_str(txt_str: &str) -> Result<Header> {
51    let mut opt_title = None;
52    let mut opt_artist = None;
53    let mut opt_bpm = None;
54    let mut opt_audio_path = None;
55
56    let mut opt_gap = None;
57    let mut opt_cover_path = None;
58    let mut opt_background_path = None;
59    let mut opt_video_path = None;
60    let mut opt_video_gap = None;
61    let mut opt_genre = None;
62    let mut opt_edition = None;
63    let mut opt_language = None;
64    let mut opt_year = None;
65    let mut opt_relative = None;
66    let mut opt_unknown: Option<HashMap<String, String>> = None;
67
68    lazy_static! {
69        static ref RE: Regex = Regex::new(r"#([A-Z3a-z]*):(.*)").unwrap();
70    }
71
72    for (line, line_count) in txt_str.lines().zip(1..) {
73        let cap = match RE.captures(line) {
74            Some(x) => x,
75            None => break,
76        };
77        let key = cap.get(1).unwrap().as_str();
78        let value = cap.get(2).unwrap().as_str();
79
80        if value == "" {
81            //TODO: somehow warn about this
82            continue;
83        }
84
85        match key {
86            "TITLE" => {
87                if opt_title.is_none() {
88                    opt_title = Some(String::from(value));
89                } else {
90                    bail!(ErrorKind::DuplicateHeader(line_count, "TITLE"));
91                }
92            }
93            "ARTIST" => {
94                if opt_artist.is_none() {
95                    opt_artist = Some(String::from(value));
96                } else {
97                    bail!(ErrorKind::DuplicateHeader(line_count, "ARTIST"));
98                }
99            }
100            "MP3" => {
101                if opt_audio_path.is_none() {
102                    opt_audio_path = Some(PathBuf::from(value));
103                } else {
104                    bail!(ErrorKind::DuplicateHeader(line_count, "MP3"));
105                }
106            }
107            "BPM" => {
108                if opt_bpm.is_none() {
109                    opt_bpm = match value.replace(",", ".").parse() {
110                        Ok(x) => Some(x),
111                        Err(_) => {
112                            bail!(ErrorKind::ValueError(line_count, "BPM"));
113                        }
114                    };
115                } else {
116                    bail!(ErrorKind::DuplicateHeader(line_count, "BPM"));
117                }
118            }
119
120            // Optional Header fields
121            "GAP" => {
122                if opt_gap.is_none() {
123                    opt_gap = match value.replace(",", ".").parse() {
124                        Ok(x) => Some(x),
125                        Err(_) => {
126                            bail!(ErrorKind::ValueError(line_count, "GAP"));
127                        }
128                    };
129                } else {
130                    bail!(ErrorKind::DuplicateHeader(line_count, "GAP"));
131                }
132            }
133            "COVER" => {
134                if opt_cover_path.is_none() {
135                    opt_cover_path = Some(PathBuf::from(value));
136                } else {
137                    bail!(ErrorKind::DuplicateHeader(line_count, "COVER"));
138                }
139            }
140            "BACKGROUND" => {
141                if opt_background_path.is_none() {
142                    opt_background_path = Some(PathBuf::from(value));
143                } else {
144                    bail!(ErrorKind::DuplicateHeader(line_count, "BACKGROUND"));
145                }
146            }
147            "VIDEO" => {
148                if opt_video_path.is_none() {
149                    opt_video_path = Some(PathBuf::from(value));
150                } else {
151                    bail!(ErrorKind::DuplicateHeader(line_count, "VIDEO"));
152                }
153            }
154            "VIDEOGAP" => {
155                if opt_video_gap.is_none() {
156                    opt_video_gap = match value.replace(",", ".").parse() {
157                        Ok(x) => Some(x),
158                        Err(_) => {
159                            bail!(ErrorKind::ValueError(line_count, "VIDEOGAP"));
160                        }
161                    };
162                } else {
163                    bail!(ErrorKind::DuplicateHeader(line_count, "VIDEOGAP"));
164                }
165            }
166            "GENRE" => {
167                if opt_genre.is_none() {
168                    opt_genre = Some(String::from(value));
169                } else {
170                    bail!(ErrorKind::DuplicateHeader(line_count, "GENRE"));
171                }
172            }
173            "EDITION" => {
174                if opt_edition.is_none() {
175                    opt_edition = Some(String::from(value));
176                } else {
177                    bail!(ErrorKind::DuplicateHeader(line_count, "EDITION"));
178                }
179            }
180            "LANGUAGE" => {
181                if opt_language.is_none() {
182                    opt_language = Some(String::from(value));
183                } else {
184                    bail!(ErrorKind::DuplicateHeader(line_count, "LANGUAGE"));
185                }
186            }
187            "YEAR" => {
188                if opt_year.is_none() {
189                    opt_year = match value.parse() {
190                        Ok(x) => Some(x),
191                        Err(_) => {
192                            bail!(ErrorKind::ValueError(line_count, "YEAR"));
193                        }
194                    };
195                } else {
196                    bail!(ErrorKind::DuplicateHeader(line_count, "YEAR"));
197                }
198            }
199            //TODO: check if relative changes line breaks
200            "RELATIVE" => {
201                if opt_relative.is_none() {
202                    opt_relative = match value {
203                        "YES" | "yes" => Some(true),
204                        "NO" | "no" => Some(false),
205                        _ => {
206                            bail!(ErrorKind::ValueError(line_count, "RELATIVE"));
207                        }
208                    }
209                } else {
210                    bail!(ErrorKind::DuplicateHeader(line_count, "RELATIVE"));
211                }
212            }
213            // use hashmap to store unknown tags
214            k => {
215                opt_unknown = match opt_unknown {
216                    Some(mut x) => {
217                        if !x.contains_key(k) {
218                            x.insert(String::from(k), String::from(value));
219                            Some(x)
220                        } else {
221                            bail!(ErrorKind::DuplicateHeader(line_count, "UNKNOWN"));
222                        }
223                    }
224                    None => {
225                        let mut unknown = HashMap::new();
226                        unknown.insert(String::from(k), String::from(value));
227                        Some(unknown)
228                    }
229                };
230            }
231        };
232    }
233
234    // build header from Options
235    if let (Some(title), Some(artist), Some(bpm), Some(audio_path)) =
236        (opt_title, opt_artist, opt_bpm, opt_audio_path)
237    {
238        let header = Header {
239            title,
240            artist,
241            bpm,
242            audio_path,
243
244            gap: opt_gap,
245            cover_path: opt_cover_path,
246            background_path: opt_background_path,
247            video_path: opt_video_path,
248            video_gap: opt_video_gap,
249            genre: opt_genre,
250            edition: opt_edition,
251            language: opt_language,
252            year: opt_year,
253            relative: opt_relative,
254            unknown: opt_unknown,
255        };
256        // header complete
257        Ok(header)
258    } else {
259        // essential field is missing
260        bail!(ErrorKind::MissingEssential)
261    }
262}
263
264/// Parses the lyric lines of a given Ultarstar song and returns a vector of Line structs
265///
266/// # Arguments
267/// * txt_str  - a &str that contains the song to parse
268///
269pub fn parse_txt_lines_str(txt_str: &str) -> Result<Vec<Line>> {
270    lazy_static! {
271        static ref LINE_RE: Regex = Regex::new("^-\\s?(-?[0-9]+)\\s*$").unwrap();
272        static ref LREL_RE: Regex = Regex::new("^-\\s?(-?[0-9]+)\\s+(-?[0-9]+)").unwrap();
273        static ref NOTE_RE: Regex =
274            Regex::new("^(.)\\s*(-?[0-9]+)\\s+(-?[0-9]+)\\s+(-?[0-9]+)\\s?(.*)").unwrap();
275        static ref DUET_RE: Regex = Regex::new("^P\\s?(-?[0-9]+)").unwrap();
276    }
277
278    let mut lines_vec = Vec::new();
279    let mut current_line = Line {
280        start: 0,
281        rel: None,
282        notes: Vec::new(),
283    };
284
285    let mut found_end_indicator = false;
286    for (line, line_count) in txt_str.lines().zip(1..) {
287        let first_char = match line.chars().nth(0) {
288            Some(x) => x,
289            None => bail!(ErrorKind::ParserFailure(line_count)),
290        };
291
292        // ignore header
293        if first_char == '#' {
294            continue;
295        }
296
297        // not implemented
298        if first_char == 'B' {
299            bail!(ErrorKind::NotImplemented(line_count, "variable bpm"));
300        }
301
302        // stop parsing after end symbol
303        if first_char == 'E' {
304            lines_vec.push(current_line);
305            found_end_indicator = true;
306            break;
307        }
308
309        // current line is a note
310        if NOTE_RE.is_match(line) {
311            let cap = NOTE_RE.captures(line).unwrap();
312
313            let note_start = match cap.get(2).unwrap().as_str().parse() {
314                Ok(x) => x,
315                Err(_) => {
316                    bail!(ErrorKind::ValueError(line_count, "note start"));
317                }
318            };
319            let note_duration = match cap.get(3).unwrap().as_str().parse() {
320                Ok(x) => {
321                    if x >= 0 {
322                        x
323                    } else {
324                        bail!(ErrorKind::ValueError(line_count, "note duration"));
325                    }
326                }
327                Err(_) => {
328                    bail!(ErrorKind::ValueError(line_count, "note duration"));
329                }
330            };
331            let note_pitch = match cap.get(4).unwrap().as_str().parse() {
332                Ok(x) => x,
333                Err(_) => {
334                    bail!(ErrorKind::ValueError(line_count, "note pitch"));
335                }
336            };
337            let note_text = cap.get(5).unwrap().as_str();
338
339            let note = match cap.get(1).unwrap().as_str() {
340                ":" => Note::Regular {
341                    start: note_start,
342                    duration: note_duration,
343                    pitch: note_pitch,
344                    text: String::from(note_text),
345                },
346                "*" => Note::Golden {
347                    start: note_start,
348                    duration: note_duration,
349                    pitch: note_pitch,
350                    text: String::from(note_text),
351                },
352                "F" => Note::Freestyle {
353                    start: note_start,
354                    duration: note_duration,
355                    pitch: note_pitch,
356                    text: String::from(note_text),
357                },
358                _ => bail!(ErrorKind::UnknownNoteType(line_count)),
359            };
360
361            current_line.notes.push(note);
362            continue;
363        }
364
365        // current line is a line break
366        if LINE_RE.is_match(line) {
367            // push old line to the Line vector and prepare new line
368            lines_vec.push(current_line);
369            let cap = LINE_RE.captures(line).unwrap();
370            let line_start = match cap.get(1).unwrap().as_str().parse() {
371                Ok(x) => x,
372                Err(_) => {
373                    bail!(ErrorKind::ValueError(line_count, "line start"));
374                }
375            };
376            current_line = Line {
377                start: line_start,
378                rel: None,
379                notes: Vec::new(),
380            };
381            continue;
382        }
383
384        // current line is a relative line break
385        if LREL_RE.is_match(line) {
386            // push old line to the Line vector and prepare new line
387            lines_vec.push(current_line);
388            let cap = LREL_RE.captures(line).unwrap();
389            let line_start = match cap.get(1).unwrap().as_str().parse() {
390                Ok(x) => x,
391                Err(_) => {
392                    bail!(ErrorKind::ValueError(line_count, "line start"));
393                }
394            };
395            let line_rel = match cap.get(2).unwrap().as_str().parse() {
396                Ok(x) => x,
397                Err(_) => {
398                    bail!(ErrorKind::ValueError(line_count, "line rel"));
399                }
400            };
401            current_line = Line {
402                start: line_start,
403                rel: Some(line_rel),
404                notes: Vec::new(),
405            };
406            continue;
407        }
408
409        if DUET_RE.is_match(line) {
410            let cap = DUET_RE.captures(line).unwrap();
411            let note = match cap.get(1).unwrap().as_str().parse() {
412                Ok(x) => {
413                    if x >= 1 && x <= 3 {
414                        Note::PlayerChange { player: x }
415                    } else {
416                        bail!(ErrorKind::ValueError(line_count, "player change"));
417                    }
418                }
419                Err(_) => {
420                    bail!(ErrorKind::ValueError(line_count, "player change"));
421                }
422            };
423            current_line.notes.push(note);
424            continue;
425        } else {
426            // unknown line
427            bail!(ErrorKind::ParserFailure(line_count));
428        }
429    }
430    if found_end_indicator {
431        Ok(lines_vec)
432    } else {
433        bail!(ErrorKind::MissingEndIndicator);
434    }
435}