termusiclib/playlist/
mod.rs1mod asx;
6mod m3u;
7mod pls;
8mod xspf;
9
10use std::{
11 borrow::Cow,
12 fmt::Display,
13 path::{Path, PathBuf},
14 str::FromStr,
15};
16
17use anyhow::{anyhow, Context, Result};
18use reqwest::Url;
19
20use crate::utils;
21
22#[derive(Debug, PartialEq, Eq, Clone)]
23#[allow(clippy::module_name_repetitions)]
24pub enum PlaylistValue {
25 Path(PathBuf),
27 Url(Url),
29}
30
31impl From<PathBuf> for PlaylistValue {
32 fn from(value: PathBuf) -> Self {
33 Self::Path(value)
34 }
35}
36
37impl Display for PlaylistValue {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 PlaylistValue::Path(v) => v.display().fmt(f),
41 PlaylistValue::Url(v) => v.fmt(f),
42 }
43 }
44}
45
46impl PlaylistValue {
47 pub fn file_url_to_path(&mut self) -> Result<()> {
53 let Self::Url(url) = self else {
54 return Ok(());
56 };
57
58 if url.scheme() == "file" {
59 let as_path = url
60 .to_file_path()
61 .map_err(|()| anyhow!("Failed to convert URL to Path!"))
62 .with_context(|| url.to_string())?;
63 *self = Self::Path(as_path);
64 }
65
66 Ok(())
67 }
68
69 pub fn absoluteize(&mut self, base: &Path) {
73 let Self::Path(path) = self else {
74 return;
75 };
76
77 if path.is_absolute() {
79 return;
80 }
81
82 if let Cow::Owned(new_path) = utils::absolute_path_base(path, base) {
84 *path = new_path;
85 }
86 }
87
88 pub fn try_from_str(line: &str) -> Result<Self> {
90 if line.contains("://") {
92 return Ok(Self::Url(Url::parse(line)?));
93 }
94
95 Ok(Self::Path(PathBuf::from_str(line)?))
96 }
97}
98
99pub fn decode(content: &str) -> Result<Vec<PlaylistValue>> {
129 let mut set: Vec<PlaylistValue> = vec![];
130 let content_small = content.to_lowercase();
131
132 if content_small.contains("<playlist") {
133 let items = xspf::decode(content)?;
134 set.reserve(items.len());
135 for item in items {
136 set.push(item.location);
137 }
138 } else if content_small.contains("<asx") {
139 let items = asx::decode(content)?;
140 set.reserve(items.len());
141 for item in items {
142 set.push(item.location);
143 }
144 } else if content_small.contains("[playlist]") {
145 let items = pls::decode(content);
146 set.reserve(items.len());
147 for item in items {
148 set.push(item.url);
149 }
150 } else {
151 let items = m3u::decode(content);
152 set.reserve(items.len());
153 for item in items {
154 set.push(item.url);
155 }
156 }
157
158 Ok(set)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use pretty_assertions::assert_eq;
165
166 #[test]
167 fn should_parse_xspf() {
168 let s = r#"<?xml version="1.0" encoding="UTF-8"?>
169 <playlist version="1" xmlns="http://xspf.org/ns/0/">
170 <trackList>
171 <track>
172 <title>Title</title>
173 <identifier>Identifier</identifier>
174 <location>http://this.is.an.example</location>
175 </track>
176 </trackList>
177 </playlist>"#;
178 let items = decode(s).unwrap();
179 assert_eq!(items.len(), 1);
180 assert_eq!(
181 items[0],
182 PlaylistValue::Url(Url::parse("http://this.is.an.example").unwrap())
183 );
184 }
185
186 #[test]
187 fn should_parse_asx() {
188 let s = r#"<asx version="3.0">
189 <title>Test-Liste</title>
190 <entry>
191 <title>title1</title>
192 <ref href="ref1"/>
193 </entry>
194</asx>"#;
195 let items = decode(s).unwrap();
196 assert_eq!(items.len(), 1);
197 assert_eq!(items[0], PlaylistValue::Path("ref1".into()));
198 }
199
200 #[test]
201 fn should_parse_pls() {
202 let items = decode(
203 "[playlist]
204File1=http://this.is.an.example
205Title1=mytitle
206 ",
207 )
208 .unwrap();
209 assert_eq!(items.len(), 1);
210 assert_eq!(
211 items[0],
212 PlaylistValue::Url(Url::parse("http://this.is.an.example").unwrap())
213 );
214 }
215
216 #[test]
217 fn should_parse_m3u() {
218 let playlist = "/some/absolute/unix/path.mp3";
219
220 let results = decode(playlist).unwrap();
221 assert_eq!(results.len(), 1);
222 assert_eq!(
223 results[0],
224 PlaylistValue::Path("/some/absolute/unix/path.mp3".into())
225 );
226 }
227
228 mod playlist_value {
229 use std::path::Path;
230
231 use reqwest::Url;
232
233 use super::super::PlaylistValue;
234
235 #[test]
237 #[cfg(target_family = "unix")]
238 fn file_url_to_path() {
239 let mut value = PlaylistValue::Url(Url::parse("file:///mnt/somewhere").unwrap());
240 value.file_url_to_path().unwrap();
241 assert_eq!(value, PlaylistValue::Path("/mnt/somewhere".into()));
242 }
243
244 #[test]
245 #[cfg(target_family = "windows")]
246 fn file_url_to_path() {
247 let mut value = PlaylistValue::Url(Url::parse("file://C:\\somewhere").unwrap());
248 value.file_url_to_path().unwrap();
249 assert_eq!(value, PlaylistValue::Path("C:\\somewhere".into()));
250 }
251
252 #[test]
253 fn file_url_to_path_path_noop() {
254 let mut value = PlaylistValue::Path("/mnt/somewhere".into());
255 value.file_url_to_path().unwrap();
256 assert_eq!(value, PlaylistValue::Path("/mnt/somewhere".into()));
257 }
258
259 #[test]
260 fn file_url_to_path_not_file() {
261 let url = Url::parse("http://google.com").unwrap();
262 let mut value = PlaylistValue::Url(url.clone());
263 value.file_url_to_path().unwrap();
264 assert_eq!(value, PlaylistValue::Url(url));
265 }
266
267 #[test]
268 fn absoluteize() {
269 let mut value = PlaylistValue::Path("somewhere".into());
270 value.absoluteize(Path::new("/tmp"));
271 assert_eq!(value, PlaylistValue::Path("/tmp/somewhere".into()));
272 }
273
274 #[test]
275 fn absoluteize_absolute() {
276 let mut value = PlaylistValue::Path("/mnt/somewhere".into());
277 value.absoluteize(Path::new("/tmp"));
278 assert_eq!(value, PlaylistValue::Path("/mnt/somewhere".into()));
279 }
280
281 #[test]
282 fn absoluteize_not_path() {
283 let url = Url::parse("file:///mnt/somewhere").unwrap();
284 let mut value = PlaylistValue::Url(url.clone());
285 value.absoluteize(Path::new("/tmp"));
286 assert_eq!(value, PlaylistValue::Url(url));
287 }
288
289 #[test]
290 fn from_line() {
291 let line = "somewhere";
292 assert_eq!(
293 PlaylistValue::try_from_str(line).unwrap(),
294 PlaylistValue::Path("somewhere".into())
295 );
296
297 let line = "/mnt/somewhere";
298 assert_eq!(
299 PlaylistValue::try_from_str(line).unwrap(),
300 PlaylistValue::Path("/mnt/somewhere".into())
301 );
302
303 let line = "";
304 assert_eq!(
305 PlaylistValue::try_from_str(line).unwrap(),
306 PlaylistValue::Path("".into())
307 );
308
309 let line = "file:///mnt/somewhere";
310 assert_eq!(
311 PlaylistValue::try_from_str(line).unwrap(),
312 PlaylistValue::Url(Url::parse("file:///mnt/somewhere").unwrap())
313 );
314
315 let line = "https://google.com";
316 assert_eq!(
317 PlaylistValue::try_from_str(line).unwrap(),
318 PlaylistValue::Url(Url::parse("https://google.com").unwrap())
319 );
320 }
321 }
322}