use crate::common::{Playlist, Attribution, Meta, Extension};
use std::{
io::Write,
borrow::Cow,
path::Path,
fs::File,
};
use xml::{
common::XmlVersion,
namespace::Namespace,
writer::{
XmlEvent,
EventWriter,
Error,
EmitterConfig,
events::StartElementBuilder,
},
};
fn print_plain<W: Write>(w: &mut EventWriter<W>, elem: &str, value: &str)
-> Result<(), Error> {
w.write(XmlEvent::start_element(elem))?;
w.write(XmlEvent::characters(value))?;
w.write(XmlEvent::end_element().name(elem))?;
Ok(())
}
fn print_option<W: Write>(w: &mut EventWriter<W>, n: &str, v: &Option<String>)
-> Result<(), Error> {
if let Some(x) = v { print_plain(w, n, x)? }
Ok(())
}
fn print_vec<W: Write>(w: &mut EventWriter<W>, n: &str, v: &Vec<String>)
-> Result<(), Error> {
for x in v {
print_plain(w, n, x.as_str())?;
}
Ok(())
}
fn print_num<W: Write>(w: &mut EventWriter<W>, name: &str, v: &Option<u64>)
-> Result<(), Error> {
if let Some(x) = v {
let mut buf = [0_u8; 30];
let mut i = 0;
let mut n = *x;
loop {
let d = (n % 10) as u8;
n /= 10;
i += 1;
buf[buf.len() - i] = d + b'0';
if n == 0 { break }
}
print_plain(w, name, std::str::from_utf8(&buf[buf.len()-i..])
.expect("print_num generated invalid utf8!"))?;
}
Ok(())
}
fn print_meta<W: Write>(w: &mut EventWriter<W>,
name: &str,
ms: &Vec<Meta>)
-> Result<(), Error> {
for m in ms {
w.write(XmlEvent::start_element(name).attr("rel", &m.rel))?;
w.write(XmlEvent::characters(&m.content))?;
w.write(XmlEvent::end_element().name(name))?;
}
Ok(())
}
fn include_ns<'a>(elm: StartElementBuilder<'a>, ns: &Namespace)
-> StartElementBuilder<'a> {
let mut bld = elm;
for (prefix, uri) in ns {
bld = bld.ns(prefix, uri);
}
bld
}
fn print_extension_elem<W: Write>(w: &mut EventWriter<W>, ext: &Extension)
-> Result<(), Error> {
w.write(include_ns(
XmlEvent::start_element("extension")
.attr("application", &ext.application),
&ext.namespace))?;
for ev in &ext.content {
if let Some(wev) = ev.as_writer_event() {
w.write(wev)?;
} else {
}
}
w.write(XmlEvent::end_element().name("extension"))?;
Ok(())
}
fn print_extension<W: Write>(w: &mut EventWriter<W>, exts: &Vec<Extension>)
-> Result<(), Error> {
for ext in exts {
print_extension_elem(w, ext)?;
}
Ok(())
}
fn print_playlist<W: Write>(w: &mut EventWriter<W>, p: &Playlist)
-> Result<(), Error> {
use XmlEvent::{EndElement, StartDocument};
use print_option as o;
use print_vec as v;
use print_num as n;
w.write(StartDocument{
version: XmlVersion::Version10,
encoding: Some("UTF-8"),
standalone: None,
})?;
w.write(XmlEvent::start_element("playlist")
.attr("version", "1")
.default_ns("http://xspf.org/ns/0/"))?;
{
o(w, "title", &p.title)?;
o(w, "creator", &p.creator)?;
o(w, "annotation", &p.annotation)?;
o(w, "info", &p.info)?;
o(w, "location", &p.location)?;
o(w, "identifier", &p.identifier)?;
o(w, "image", &p.image)?;
o(w, "date", &p.date)?;
o(w, "license", &p.license)?;
if !p.attribution.is_empty() {
w.write(XmlEvent::start_element("attribution"))?;
for attrib in &p.attribution {
match attrib {
Attribution::Location(s) => print_plain(w, "location", s)?,
Attribution::Identifier(s) => print_plain(w, "identifier", s)?,
}
}
w.write(XmlEvent::end_element().name("attribution"))?;
}
print_meta(w, "link", &p.link)?;
print_meta(w, "meta", &p.meta)?;
print_extension(w, &p.extension)?;
}
w.write(XmlEvent::start_element("trackList"))?;
for t in &p.track_list {
w.write(XmlEvent::start_element("track"))?;
v(w, "location", &t.location)?;
v(w, "identifier", &t.identifier)?;
o(w, "title", &t.title)?;
o(w, "creator", &t.creator)?;
o(w, "annotation", &t.annotation)?;
o(w, "info", &t.info)?;
o(w, "image", &t.image)?;
o(w, "album", &t.album)?;
n(w, "trackNum", &t.track_num)?;
n(w, "duration", &t.duration)?;
print_meta(w, "link", &t.link)?;
print_meta(w, "meta", &t.meta)?;
print_extension(w, &t.extension)?;
w.write(XmlEvent::end_element().name("track"))?;
}
w.write(XmlEvent::end_element().name("trackList"))?;
w.write(EndElement{
name: Some("playlist".into()),
})?;
Ok(())
}
fn default_cfg() -> EmitterConfig {
EmitterConfig::default()
.keep_element_names_stack(false)
.pad_self_closing(false)
}
impl Playlist {
pub fn write_to<W: Write>(&self, w: W) -> Result<(), Error> {
print_playlist(&mut EventWriter::new(w), self)
}
pub fn write_to_pretty<W: Write, S: Into<Cow<'static, str>>>(&self, w: W, indent: S) -> Result<(), Error> {
self.write_to_with_config(w, default_cfg()
.perform_indent(true)
.indent_string(indent))
}
fn write_to_with_config<W: Write>(&self, w: W, cfg: EmitterConfig)
-> Result<(), Error> {
print_playlist(&mut EventWriter::new_with_config(w, cfg), self)
}
pub fn write_file<P: AsRef<Path>>(&self, filepath: P) -> Result<(), Error> {
let f = File::create(filepath)?;
self.write_to(f)
}
#[must_use] pub fn to_string(&self) -> String {
self.to_string_with_config(default_cfg())
}
fn to_string_with_config(&self, cfg: EmitterConfig) -> String {
let mut v: Vec<u8> = vec![];
self.write_to_with_config(&mut v, cfg).unwrap();
String::from_utf8(v).unwrap()
}
pub fn to_string_pretty<S: Into<Cow<'static, str>>>(&self, indent: S) -> String {
self.to_string_with_config(default_cfg()
.perform_indent(true)
.indent_string(indent))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::Track;
#[test]
fn print_simple() {
let track1 = Track{
title: Some("Track1".to_string()),
track_num: Some(1),
duration: Some(0),
location: vec!["file:///dev/null".to_string()],
.. Track::default() };
let mut p1 = Playlist::default();
p1.identifier = Some("foo:bar".to_string());
p1.accredit();
p1.identifier = Some("foo:newbar".to_string());
p1.title = Some("sometitle".to_string());
p1.creator =
Some("Cool Person <coolperson@email.example.org>".to_string());
p1.track_list.push(track1);
p1.meta.push(Meta{
rel: "http://example.org/ns/1".to_string(),
content: "foobar".to_string()
});
assert_eq!(
p1.to_string_pretty(" "),
r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" version="1">
<title>sometitle</title>
<creator>Cool Person <coolperson@email.example.org></creator>
<identifier>foo:newbar</identifier>
<attribution>
<identifier>foo:bar</identifier>
</attribution>
<meta rel="http://example.org/ns/1">foobar</meta>
<trackList>
<track>
<location>file:///dev/null</location>
<title>Track1</title>
<trackNum>1</trackNum>
<duration>0</duration>
</track>
</trackList>
</playlist>"#);
}
#[test]
fn do_roundtrips() {
let s1 = r#"<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" version="1">
<title>Playlist</title>
<extension xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" application="http://www.videolan.org/vlc/playlist/0">
<vlc:item tid="0"/>
<vlc:item tid="1"/>
</extension>
<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 xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" 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 xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" application="http://www.videolan.org/vlc/playlist/0">
<vlc:id>1</vlc:id>
<vlc:option>network-caching=1000</vlc:option>
</extension>
</track>
</trackList>
</playlist>"#;
let p1: Playlist = s1.parse().unwrap();
let r1 = p1.to_string_pretty(" ");
if r1 != s1 {
println!("=== original ===\n{s1}");
println!("=== modified ===\n{r1}");
assert!(false);
}
}
}