serde_m3u/
lib.rs

1use serde::{Deserialize, Serialize};
2
3const EXTM3U: &str = "#EXTM3U";
4const EXTINF: &str = "#EXTINF";
5
6#[derive(Serialize, Deserialize, Debug, Clone)]
7pub struct Entry {
8    pub title: Option<String>,
9    pub url: String,
10    pub time: Option<i32>,
11}
12impl core::fmt::Display for Entry {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        let info = match (self.time, self.title.clone()) {
15            (Some(d), Some(t)) => {
16                format!("{EXTINF}:{},{}\n{}", d, t, self.url)
17            }
18            (None, Some(t)) => {
19                format!("{EXTINF}:0,{}\n{}", t, self.url)
20            }
21            (Some(d), None) => {
22                format!("{EXTINF}:{}", d)
23            }
24            (None, None) => self.url.to_string(),
25        };
26        f.write_str(info.as_str())
27    }
28}
29
30#[derive(Serialize, Deserialize, Debug, Clone)]
31pub struct Playlist {
32    pub list: Vec<Entry>,
33}
34
35impl core::fmt::Display for Playlist {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        let s = format!(
38            "{EXTM3U}\n{}",
39            self.list
40                .iter()
41                .map(|i| i.to_string())
42                .collect::<Vec<_>>()
43                .join("\n")
44        );
45        f.write_str(s.as_str())
46    }
47}
48
49impl<'a> From<&'a str> for Playlist {
50    fn from(value: &'a str) -> Self {
51        let mut list: Vec<Entry> = vec![];
52        let mut lines = value.lines();
53
54        let first = lines.next();
55        if let Some(s) = first {
56            if s != EXTM3U {
57                list.push(Entry {
58                    url: s.to_string(),
59                    title: None,
60                    time: None,
61                });
62                for i in lines {
63                    list.push(Entry {
64                        url: i.to_string(),
65                        title: None,
66                        time: None,
67                    })
68                }
69            } else {
70                while let (Some(info), Some(url)) = (lines.next(), lines.next()) {
71                    if let (Some(header_index), Some(title_index)) =
72                        (info.find(':'), info.find(','))
73                    {
74                        let time: i32 = info[header_index + 1..title_index]
75                            .parse()
76                            .unwrap_or_default();
77                        let title = info[title_index + 1..].to_string();
78                        list.push(Entry {
79                            url: url.to_string(),
80                            title: Some(title),
81                            time: Some(time),
82                        })
83                    }
84                }
85            }
86        }
87        Self { list }
88    }
89}
90
91#[cfg(test)]
92mod test {
93    use crate::Playlist;
94
95    #[test]
96    pub fn base() {
97        let s = r#"
98#EXTM3U
99#EXTINF:419,Alice in Chains - Rotten Apple
100Alice in Chains_Jar of Flies_01_Rotten Apple.mp3
101#EXTINF:260,Alice in Chains - Nutshell
102Alice in Chains_Jar of Flies_02_Nutshell.mp3
103"#
104        .trim();
105
106        let playlist = Playlist::from(s);
107
108        assert_eq!(playlist.list.len(), 2);
109        assert_eq!(playlist.to_string(), s);
110    }
111}