xspf 0.4.2

straightforward pure rust implementation of the Xml Sharable Playlist Format
Documentation
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,
	/// missing rel attribute from `<link>` or `<meta>`
	MissingRel,
	/// missing application attribute from `<extension>`
	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
    }
}

/// for tags whose only data is their inner text
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)?),
						_ => {},
						//n => println!("ignoring element: {:?}", n),
					}
				}
			},
			XmlEvent::EndElement{ name } => {
				if name.prefix.is_none() && name.local_name == "track" {
    						return Ok(t);
    					}
			},
			XmlEvent::EndDocument => return Err(ParseError::UnexpectedEOF),
			_ev => {
				//println!("ignoring event: {:?}", _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" => {
							// for now, we ignore this level
						}
						"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)?),
						// ignore unknown element names
						_ => {}
					}
				}
			},
			XmlEvent::EndDocument => return Ok(pl),
			_ev => {
				//println!("ignoring event: {:?}", ev);
			},
		}
	}
}

impl Playlist {
	/// read and parse xml from the specified `io::Read` implementation
	pub fn read_from<R: Read>(r: R) -> Result<Self, ParseError> {
		parse_playlist(&mut EventReader::new(r))
	}

	/// read and parse xml from the specified file
	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() {
		// make sure EOF never causes an infinite loop
		for i in 0..VLC_SAMPLE.len() {
			let _r1: Result<Playlist, _>  = VLC_SAMPLE[..i].parse();
		}
	}
}