1extern crate peg;
2
3use peg::error::ParseError;
4use peg::str::LineCol;
5
6#[derive(Debug, PartialEq, serde::Serialize)]
7pub struct M3UEntry {
8 pub title: String,
9 pub duration: String,
10 pub path: String,
11}
12
13peg::parser! {
14 pub grammar m3u_parser() for str {
15 rule m3u() -> Vec<M3UEntry>
16 = _ "#EXTM3U" _ (!"#EXTINF:" [_])* entries:(line())* { entries }
17
18 rule format_duration() -> String
19 = seconds:digits() {
20 let secs: u64 = seconds.parse().unwrap_or(0);
21 let minutes = secs / 60;
22 let remaining_seconds = secs % 60;
23 format!("{:02}:{:02}", minutes, remaining_seconds)
24 }
25
26 pub rule url() -> String
27 = protocol:$(['a'..='z' | 'A'..='Z']+) "://" rest:$([^'\n']* "\n") {
28 format!("{}://{}", protocol, rest)
29 }
30
31 pub rule path() -> String
32 = rest:$([^'\n']* "\n") { rest.to_string() }
33
34 rule line() -> M3UEntry
35 = _ "#EXTINF:" duration:format_duration() "," title:$([^'\n']* "\n") path:(url() / path()) {
36 M3UEntry {
37 title: title.trim_end_matches('\n').trim_end_matches('\r').to_string(),
38 duration: duration,
39 path: path.trim_end_matches('\n').trim_end_matches('\r').to_string(),
40 }
41 }
42
43 rule digits() -> String
44 = digits:$(['0'..='9']+) { digits.to_string() }
45
46 rule _()
47 = quiet!{ [' ' | '\t' | '\r']* }
48
49 pub rule parse_m3u() -> Vec<M3UEntry>
50 = m3u()
51 }
52}
53
54pub fn parse_m3u(m3u_content: &str) -> Result<Vec<M3UEntry>, ParseError<LineCol>> {
55 m3u_parser::parse_m3u(m3u_content)
56}