diff --git a/src/playlistparsers/xspfparser.cpp b/src/playlistparsers/xspfparser.cpp index ac398725d..b9492b17f 100644 --- a/src/playlistparsers/xspfparser.cpp +++ b/src/playlistparsers/xspfparser.cpp @@ -16,8 +16,10 @@ #include "xspfparser.h" +#include #include #include +#include #include #include @@ -137,5 +139,52 @@ Song XSPFParser::ParseTrack(QXmlStreamReader* reader) const { } void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir) const { + QDomDocument doc; + QDomElement root = doc.createElement("playlist"); + doc.appendChild(root); + QDomElement track_list = doc.createElement("trackList"); + root.appendChild(track_list); + foreach (const Song& song, songs) { + QString url; + if (song.filetype() == Song::Type_Stream) { + url = song.filename(); + } else { + url = QUrl::fromLocalFile(MakeRelativeTo(song.filename(), dir)).toString(); + } + if (url.isEmpty()) { + continue; // Skip empty items like Last.fm streams. + } + QDomElement track = doc.createElement("track"); + track_list.appendChild(track); + MaybeAppendElementWithText("location", url, &doc, &track); + MaybeAppendElementWithText("creator", song.artist(), &doc, &track); + MaybeAppendElementWithText("album", song.album(), &doc, &track); + MaybeAppendElementWithText("title", song.title(), &doc, &track); + if (song.length() != -1) { + MaybeAppendElementWithText("duration", QString::number(song.length() * 1000), &doc, &track); + } + QString art = song.art_manual().isEmpty() ? song.art_automatic() : song.art_manual(); + // Ignore images that are in our resource bundle. + if (!art.startsWith(":") && !art.isEmpty()) { + // Convert local files to URLs. + if (!art.contains(QRegExp("^\\w+://"))) { + art = QUrl::fromLocalFile(MakeRelativeTo(art, dir)).toString(); + } + MaybeAppendElementWithText("image", art, &doc, &track); + } + } + device->write(""); + device->write(doc.toByteArray(2)); +} + +void XSPFParser::MaybeAppendElementWithText( + const QString& element_name, const QString& text, QDomDocument* doc, QDomNode* parent) const { + if (text.isEmpty()) { + return; + } + QDomElement element = doc->createElement(element_name); + QDomText t = doc->createTextNode(text); + element.appendChild(t); + parent->appendChild(element); } diff --git a/src/playlistparsers/xspfparser.h b/src/playlistparsers/xspfparser.h index 8365f51b0..73ad6d48d 100644 --- a/src/playlistparsers/xspfparser.h +++ b/src/playlistparsers/xspfparser.h @@ -21,6 +21,9 @@ #include +class QDomDocument; +class QDomNode; + class XSPFParser : public ParserBase { Q_OBJECT @@ -37,6 +40,8 @@ class XSPFParser : public ParserBase { bool ParseUntilElement(QXmlStreamReader* reader, const QString& element) const; void IgnoreElement(QXmlStreamReader* reader) const; Song ParseTrack(QXmlStreamReader* reader) const; + void MaybeAppendElementWithText( + const QString& element, const QString& text, QDomDocument* doc, QDomNode* parent) const; }; #endif diff --git a/tests/xspfparser_test.cpp b/tests/xspfparser_test.cpp index 2fe0fc2ed..661806046 100644 --- a/tests/xspfparser_test.cpp +++ b/tests/xspfparser_test.cpp @@ -15,12 +15,15 @@ */ #include "test_utils.h" +#include "gmock/gmock-matchers.h" #include "gtest/gtest.h" #include "playlistparsers/xspfparser.h" #include +using ::testing::HasSubstr; + class XSPFParserTest : public ::testing::Test { }; @@ -89,3 +92,24 @@ TEST_F(XSPFParserTest, IgnoresInvalidLength) { ASSERT_EQ(1, songs.length()); EXPECT_EQ(-1, songs[0].length()); } + +TEST_F(XSPFParserTest, SavesSong) { + QByteArray data; + QBuffer buffer(&data); + buffer.open(QIODevice::WriteOnly); + XSPFParser parser; + Song one; + one.set_filename("http://www.example.com/foo.mp3"); + one.set_filetype(Song::Type_Stream); + one.set_title("foo"); + one.set_length(123); + one.set_artist("bar"); + SongList songs; + songs << one; + + parser.Save(songs, &buffer); + EXPECT_THAT(data.constData(), HasSubstr("http://www.example.com/foo.mp3")); + EXPECT_THAT(data.constData(), HasSubstr("123000")); + EXPECT_THAT(data.constData(), HasSubstr("foo")); + EXPECT_THAT(data.constData(), HasSubstr("bar")); +}