use crate::common::{Playlist, Track, Attribution, Meta, Extension};
use std::str::FromStr;
use std::num::ParseIntError;
use std::io::Read;
use std::path::Path;
use std::fs::File;
use xml::{
EventReader,
reader::XmlEvent,
name::OwnedName,
attribute::OwnedAttribute,
namespace::Namespace,
};
#[derive(Debug)]
#[non_exhaustive]
#[must_use]
pub enum ParseError {
XmlError(xml::reader::Error),
IoError(std::io::Error),
InvalidInteger,
UnexpectedEvent(XmlEvent),
UnexpectedEOF,
MissingRel,
MissingApplication,
}
impl From<xml::reader::Error> for ParseError {
fn from(e: xml::reader::Error) -> Self { Self::XmlError(e) }
}
impl From<std::io::Error> for ParseError {
fn from(e: std::io::Error) -> Self { Self::IoError(e) }
}
impl From<ParseIntError> for ParseError {
fn from(_e: ParseIntError) -> Self { Self::InvalidInteger }
}
fn xspf_name(n: &OwnedName) -> Option<&str> {
if n.namespace.as_deref() == Some("http://xspf.org/ns/0/") ||
(n.namespace.is_none() && (n.prefix.is_none() || n.prefix.as_deref() == Some("xspf")))
{
Some(&n.local_name)
} else {
None
}
}
fn parse_plain<R: Read>(r: &mut EventReader<R>) -> Result<String, ParseError> {
match r.next()? {
XmlEvent::Characters(s) => Ok(s),
XmlEvent::EndElement{ .. } => Ok(String::new()),
ev => Err(ParseError::UnexpectedEvent(ev)),
}
}
fn get_attr(attrs: Vec<OwnedAttribute>, name: &str) -> Option<String> {
for attr in attrs {
if xspf_name(&attr.name) == Some(name) {
return Some(attr.value);
}
}
None
}
fn parse_meta<R: Read>(r: &mut EventReader<R>, attrs: Vec<OwnedAttribute>)
-> Result<Meta, ParseError> {
if let Some(rel) = get_attr(attrs, "rel") {
Ok(Meta{
rel,
content: parse_plain(r)?,
})
} else {
Err(ParseError::MissingRel)
}
}
fn parse_extension<R: Read>(r: &mut EventReader<R>,
attrs: Vec<OwnedAttribute>,
namespace: Namespace)
-> Result<Extension, ParseError> {
let mut ext = Extension{
namespace,
application: get_attr(attrs, "application")
.ok_or(ParseError::MissingApplication)?,
content: Vec::new(),
};
loop {
let ev = r.next()?;
match &ev {
XmlEvent::EndElement{ name } => {
if xspf_name(name) == Some("extension") {
return Ok(ext);
}
}
XmlEvent::EndDocument =>
return Err(ParseError::UnexpectedEOF),
_ => {},
}
ext.content.push(ev);
}
}
fn parse_track<R: Read>(r: &mut EventReader<R>) -> Result<Track, ParseError> {
let mut t = Track::default();
loop {
match r.next()? {
XmlEvent::StartElement{ name, attributes, namespace } => {
if let Some(s) = xspf_name(&name) {
match s {
"location" => t.location.push(parse_plain(r)?),
"identifier" => t.identifier.push(parse_plain(r)?),
"title" => t.title = Some(parse_plain(r)?),
"creator" => t.creator = Some(parse_plain(r)?),
"annotation" => t.annotation = Some(parse_plain(r)?),
"info" => t.info = Some(parse_plain(r)?),
"image" => t.image = Some(parse_plain(r)?),
"album" => t.album = Some(parse_plain(r)?),
"trackNum" =>
t.track_num = Some(parse_plain(r)?.parse()?),
"duration" =>
t.duration = Some(parse_plain(r)?.parse()?),
"link" => t.link.push(parse_meta(r, attributes)?),
"meta" => t.meta.push(parse_meta(r, attributes)?),
"extension" => t.extension.push(parse_extension(
r, attributes, namespace)?),
_ => {},
}
}
},
XmlEvent::EndElement{ name } => {
if name.prefix.is_none() && name.local_name == "track" {
return Ok(t);
}
},
XmlEvent::EndDocument => return Err(ParseError::UnexpectedEOF),
_ev => {
},
}
}
}
fn parse_attribution<R: Read>(r: &mut EventReader<R>)
-> Result<Vec<Attribution>, ParseError> {
let mut at: Vec<Attribution> = vec![];
loop {
match r.next()? {
XmlEvent::StartElement{ name, .. } => {
match xspf_name(&name) {
Some("location") =>
at.push(Attribution::Location(parse_plain(r)?)),
Some("identifier") =>
at.push(Attribution::Identifier(parse_plain(r)?)),
_ => {},
}
}
XmlEvent::EndElement{ name } => {
if xspf_name(&name) == Some("attribution") {
return Ok(at);
}
}
XmlEvent::EndDocument => return Err(ParseError::UnexpectedEOF),
_ => {},
}
}
}
fn parse_playlist<R: Read>(r: &mut EventReader<R>) -> Result<Playlist, ParseError> {
let mut pl = Playlist::default();
loop {
match r.next()? {
XmlEvent::StartElement{ name, attributes, namespace } => {
if let Some(s) = xspf_name(&name) {
match s {
"title" => {
pl.title = Some(parse_plain(r)?);
}
"creator" => {
pl.creator = Some(parse_plain(r)?);
}
"annotation" => {
pl.annotation = Some(parse_plain(r)?);
}
"info" => {
pl.info = Some(parse_plain(r)?);
}
"location" => {
pl.location = Some(parse_plain(r)?);
}
"identifier" => {
pl.identifier = Some(parse_plain(r)?);
}
"image" => {
pl.image = Some(parse_plain(r)?);
}
"date" => {
pl.date = Some(parse_plain(r)?);
}
"license" => {
pl.license = Some(parse_plain(r)?);
}
"attribution" => {
pl.attribution = parse_attribution(r)?;
}
"trackList" => {
}
"track" => {
pl.track_list.push(parse_track(r)?);
}
"link" => pl.link.push(parse_meta(r, attributes)?),
"meta" => pl.meta.push(parse_meta(r, attributes)?),
"extension" =>
pl.extension.push(parse_extension(
r, attributes, namespace)?),
_ => {}
}
}
},
XmlEvent::EndDocument => return Ok(pl),
_ev => {
},
}
}
}
impl Playlist {
pub fn read_from<R: Read>(r: R) -> Result<Self, ParseError> {
parse_playlist(&mut EventReader::new(r))
}
pub fn read_file<P: AsRef<Path>>(filepath: P) -> Result<Self, ParseError> {
let f = File::open(filepath)?;
Playlist::read_from(f)
}
}
impl FromStr for Playlist {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_playlist(&mut EventReader::from_str(s))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
let example1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<title>test playlist</title>
<attribution>
<location>http://bar.example/modified\_version\_of\_original\_playlist.xspf</location>
<identifier>somescheme:original\_playlist.xspf</identifier>
</attribution>
<trackList>
<track><location>file:///music/song\_1.ogg</location><title>track one</title></track>
<track><location>file:///music/song\_2.flac</location></track>
<track><link rel="http://foaf.org/namespace/version1">http://socialnetwork.org/foaf/mary.rdfs</link><location>file:///music/song\_3.mp3</location></track>
</trackList>
</playlist>"#;
let pl1: Playlist = example1.parse().unwrap();
assert_eq!(pl1.title.unwrap(), "test playlist");
assert_eq!(pl1.track_list[2].location[0],
r"file:///music/song\_3.mp3");
assert_eq!(pl1.track_list[0].title.as_ref().unwrap(), "track one");
assert_eq!(pl1.attribution[1], Attribution::Identifier(
r"somescheme:original\_playlist.xspf".to_string()));
assert_eq!(pl1.track_list[2].link[0].rel,
"http://foaf.org/namespace/version1");
assert_eq!(pl1.track_list[2].link[0].content,
"http://socialnetwork.org/foaf/mary.rdfs");
}
#[test]
fn parse_namespaced() {
let example1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<abc:playlist xmlns:abc="http://xspf.org/ns/0/">
<abc:title>playlist for testing</abc:title>
</abc:playlist>
"#;
let pl1: Playlist = example1.parse().unwrap();
assert_eq!(pl1.title.unwrap(), "playlist for testing");
}
pub const VLC_SAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" version="1">
<title>Playlist</title>
<trackList>
<track>
<location>https://upload.wikimedia.org/wikipedia/commons/6/60/Bach_-_cantata_140._1._chorus.ogg</location>
<title>Cantata 140. 1. Chorus</title>
<creator>MIT Concert Choir (Cutter)</creator>
<album>Bach and Schubert</album>
<trackNum>8</trackNum>
<duration>347324</duration>
<extension application="http://www.videolan.org/vlc/playlist/0">
<vlc:id>0</vlc:id>
<vlc:option>network-caching=1000</vlc:option>
</extension>
</track>
<track>
<location>https://upload.wikimedia.org/wikipedia/commons/8/87/Beethoven_-_opus47-1_01.ogg</location>
<title>Beethoven:violin-piano sonatas</title>
<creator>Paul Rosenthal and Edward Auer</creator>
<duration>623046</duration>
<extension application="http://www.videolan.org/vlc/playlist/0">
<vlc:id>1</vlc:id>
<vlc:option>network-caching=1000</vlc:option>
</extension>
</track>
</trackList>
<extension application="http://www.videolan.org/vlc/playlist/0">
<vlc:item tid="0"/>
<vlc:item tid="1"/>
</extension>
</playlist>
"#;
#[test]
fn parse_vlc() {
let example1 = VLC_SAMPLE;
let pl1: Playlist = example1.parse().unwrap();
assert_eq!(pl1.title.unwrap(), "Playlist");
assert_eq!(pl1.extension[0].application, "http://www.videolan.org/vlc/playlist/0");
assert_eq!(pl1.track_list[0].extension[0].application, "http://www.videolan.org/vlc/playlist/0");
}
#[test]
fn unexpected_eof() {
for i in 0..VLC_SAMPLE.len() {
let _r1: Result<Playlist, _> = VLC_SAMPLE[..i].parse();
}
}
}