m3u_cli_parser/
lib.rs

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}