vlc_rc/client/
media.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3
4/// The minimum amount for a volume setting.
5pub const MIN_VOLUME: u8 = 0;
6
7/// The maximum amount for a volume setting.
8pub const MAX_VOLUME: u8 = 200;
9
10/// A type alias for a collection of [tracks](Track).
11pub type Playlist = Vec<Track>;
12
13/// A type alias for a collection of [subtitles](Subtitle).
14pub type Subtitles = Vec<Subtitle>;
15
16/// A trait implemented by types that can be constructed from the VLC interface's output.
17pub(crate) trait FromParts: Sized {
18    /// Attempts to construct a type from the given VLC output - returning `None` if it is not possible.
19    fn from_parts(parts: &str) -> Option<Self>;
20}
21
22/// A media track in a VLC player's [playlist](Playlist).
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Track {
25    index: i32,
26    title: String,
27    length: String,
28}
29
30impl Track {
31    /// Gets the track's index in the playlist.
32    pub fn index(&self) -> i32 {
33        self.index
34    }
35
36    /// Gets the track's title - commonly the file name.
37    pub fn title(&self) -> &str {
38        &self.title
39    }
40
41    /// Gets the track's length as `<hours>:<minutes>:<seconds>`.
42    pub fn length(&self) -> &str {
43        &self.length
44    }
45}
46
47impl std::fmt::Display for Track {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{} - {} ({})", self.index, self.title, self.length)
50    }
51}
52
53impl FromParts for Track {
54    fn from_parts(parts: &str) -> Option<Self> {
55        lazy_static! {
56            static ref REGEX: Regex = Regex::new(
57                r"(?x)
58                \| # List item delimiter.
59                \s+
60                [\*]?
61                (?P<index>[\d]+) # The track's index.
62                \s+
63                -
64                \s+
65                (?P<title>.+) # The track's title.
66                \s
67                \(
68                (?P<length>\d\d:\d\d:\d\d) # The track's length.
69                .*
70        ",
71            )
72            .unwrap();
73        };
74        let caps = REGEX.captures(parts)?;
75        Some(Self {
76            index: caps["index"].parse().ok()?,
77            title: caps["title"].to_owned(),
78            length: caps["length"].to_owned(),
79        })
80    }
81}
82
83/// A subtitle track associated with a media file.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Subtitle {
86    index: i32,
87    title: String,
88}
89
90impl std::fmt::Display for Subtitle {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        write!(f, "{} - {}", self.index, self.title)
93    }
94}
95
96impl Subtitle {
97    /// Gets the subtitle track's index in VLC.
98    pub fn index(&self) -> i32 {
99        self.index
100    }
101
102    /// Gets the subtitle track's name.
103    pub fn title(&self) -> &str {
104        &self.title
105    }
106}
107
108impl FromParts for Subtitle {
109    fn from_parts(parts: &str) -> Option<Self> {
110        lazy_static! {
111            static ref REGEX: Regex = Regex::new(
112                r"(?x)
113                \| # List item delimiter.
114                \s+
115                (?P<index>[\-]?[\d]+) # The subtitle's index.
116                \s+
117                -
118                \s+
119                (?P<title>.+) # The subtitle track's title.
120        ",
121            )
122            .unwrap();
123        };
124
125        let caps = REGEX.captures(parts)?;
126        Some(Self {
127            index: caps["index"].parse().ok()?,
128            title: caps["title"].to_owned(),
129        })
130    }
131}
132
133#[cfg(test)]
134mod test {
135    use super::*;
136
137    /// A helper macro used to test if the input matches the expected output by using the type's [`FromParts`] implementation.
138    macro_rules! test_from_parts {
139        ($in:expr, $out:expr) => {
140            assert_eq!(FromParts::from_parts($in), $out);
141        };
142
143        ($t:ident, $in:expr, $out:expr) => {
144            assert_eq!($t::from_parts($in), $out);
145        };
146    }
147
148    #[test]
149    fn track_from_parts_none() {
150        test_from_parts!(Track, "+----[ Playlist - playlist ]", None);
151        test_from_parts!(Track, "| 1 - Playlist", None);
152        test_from_parts!(Track, "| 2 - Media Library", None);
153        test_from_parts!(Track, "+----[ End of playlist ]", None);
154    }
155
156    #[test]
157    fn track_from_parts_some() {
158        test_from_parts!(
159            "| 8 - Chopin Nocturnes.mp3 (01:50:55)",
160            Some(Track {
161                index: 8,
162                title: "Chopin Nocturnes.mp3".into(),
163                length: "01:50:55".into()
164            })
165        );
166        test_from_parts!(
167            "| *1 - Bach (00:00:01).mp3 (01:50:55)",
168            Some(Track {
169                index: 1,
170                title: "Bach (00:00:01).mp3".into(),
171                length: "01:50:55".into()
172            })
173        );
174    }
175
176    #[test]
177    fn subtitle_from_parts_none() {
178        test_from_parts!(Subtitle, "+----[ spu-es ]", None);
179        test_from_parts!(Subtitle, "+----[ end of spu-es ]", None);
180    }
181
182    #[test]
183    fn subtitle_from_parts_some() {
184        test_from_parts!(
185            "| -1 - Disable *",
186            Some(Subtitle { index: -1, title: "Disable *".into() })
187        );
188        test_from_parts!(
189            "| 2 - Track 1 - [English]",
190            Some(Subtitle { index: 2, title: "Track 1 - [English]".into() })
191        );
192    }
193}