diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 07c565210..458fe1f61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,7 @@ set(CLEMENTINE-SOURCES m3uparser.cpp playlistmanager.cpp playlistsequence.cpp + xspfparser.cpp ) # Header files that have Q_OBJECT in @@ -104,6 +105,7 @@ set(CLEMENTINE-MOC-HEADERS m3uparser.h playlistmanager.h playlistsequence.h + xspfparser.h ) # UI files diff --git a/src/m3uparser.cpp b/src/m3uparser.cpp index a71352e0d..29a22f08b 100644 --- a/src/m3uparser.cpp +++ b/src/m3uparser.cpp @@ -30,6 +30,7 @@ const QList& M3UParser::Parse() { Song song; song.Init(current_metadata_.title, current_metadata_.artist, + QString(), // Unknown album. current_metadata_.length); // Track location. QString location; diff --git a/src/song.cpp b/src/song.cpp index 142118f1c..e997a3e2a 100644 --- a/src/song.cpp +++ b/src/song.cpp @@ -103,10 +103,11 @@ Song::Song(FileRefFactory* factory) factory_(factory) { } -void Song::Init(const QString& title, const QString& artist, int length) { +void Song::Init(const QString& title, const QString& artist, const QString& album, int length) { d->valid_ = true; d->title_ = title; d->artist_ = artist; + d->album_ = album; d->length_ = length; } diff --git a/src/song.h b/src/song.h index d6282a626..034536a85 100644 --- a/src/song.h +++ b/src/song.h @@ -59,7 +59,7 @@ class Song { }; // Constructors - void Init(const QString& title, const QString& artist, int length); + void Init(const QString& title, const QString& artist, const QString& album, int length); void InitFromFile(const QString& filename, int directory_id); void InitFromQuery(const QSqlQuery& query); void InitFromLastFM(const lastfm::Track& track); diff --git a/src/xspfparser.cpp b/src/xspfparser.cpp new file mode 100644 index 000000000..249ea0b60 --- /dev/null +++ b/src/xspfparser.cpp @@ -0,0 +1,113 @@ +#include "xspfparser.h" + +#include +#include +#include +#include + +XSPFParser::XSPFParser(QIODevice* device, QObject* parent) + : QObject(parent), + device_(device) { +} + +const SongList& XSPFParser::Parse() { + QXmlStreamReader reader(device_); + if (!ParseUntilElement(&reader, "playlist") || + !ParseUntilElement(&reader, "trackList")) { + return songs_; + } + + while (!reader.atEnd() && ParseUntilElement(&reader, "track")) { + Song song = ParseTrack(&reader); + if (song.is_valid()) { + songs_ << song; + } + } + return songs_; +} + +bool XSPFParser::ParseUntilElement(QXmlStreamReader* reader, const QString& name) const { + while (!reader->atEnd()) { + QXmlStreamReader::TokenType type = reader->readNext(); + switch (type) { + case QXmlStreamReader::StartElement: + if (reader->name() == name) { + return true; + } else { + IgnoreElement(reader); + } + break; + default: + break; + } + } + return false; +} + +void XSPFParser::IgnoreElement(QXmlStreamReader* reader) const { + int level = 1; + while (level != 0 && !reader->atEnd()) { + QXmlStreamReader::TokenType type = reader->readNext(); + switch (type) { + case QXmlStreamReader::StartElement: + ++level; + break; + case QXmlStreamReader::EndElement: + --level; + break; + default: + break; + } + } +} + +Song XSPFParser::ParseTrack(QXmlStreamReader* reader) const { + Song song; + QString title, artist, album; + int length = -1; + while (!reader->atEnd()) { + QXmlStreamReader::TokenType type = reader->readNext(); + switch (type) { + case QXmlStreamReader::StartElement: { + QStringRef name = reader->name(); + if (name == "location") { + QUrl url(reader->readElementText()); + if (url.scheme() == "file") { + QString filename = url.toLocalFile(); + if (!QFile::exists(filename)) { + return Song(); + } + song.InitFromFile(filename, -1); + return song; + } else { + song.set_filename(url.toString()); + song.set_filetype(Song::Type_Stream); + } + } else if (name == "title") { + title = reader->readElementText(); + } else if (name == "creator") { + artist = reader->readElementText(); + } else if (name == "album") { + album = reader->readElementText(); + } else if (name == "duration") { // in milliseconds. + const QString& duration = reader->readElementText(); + bool ok = false; + length = duration.toInt(&ok) / 1000; + if (!ok) { + return Song(); + } + song.set_length(length); + } else if (name == "image") { + // TODO: Fetch album covers. + } else if (name == "info") { + // TODO: Do something with extra info? + } + break; + } + default: + break; + } + } + song.Init(title, artist, album, length); + return song; +} diff --git a/src/xspfparser.h b/src/xspfparser.h new file mode 100644 index 000000000..082598b8e --- /dev/null +++ b/src/xspfparser.h @@ -0,0 +1,28 @@ +#ifndef XSPFPARSER_H +#define XSPFPARSER_H + +#include "song.h" + +#include + +class QIODevice; +class QXmlStreamReader; + +class XSPFParser : public QObject { + Q_OBJECT + public: + XSPFParser(QIODevice* device, QObject* parent = 0); + virtual ~XSPFParser() {} + + const SongList& Parse(); + + private: + bool ParseUntilElement(QXmlStreamReader* reader, const QString& element) const; + void IgnoreElement(QXmlStreamReader* reader) const; + Song ParseTrack(QXmlStreamReader* reader) const; + + QIODevice* device_; + SongList songs_; +}; + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 17ce0c0e2..763dabc70 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -65,3 +65,4 @@ add_test_file(m3uparser_test.cpp) add_test_file(song_test.cpp) add_test_file(librarybackend_test.cpp) add_test_file(albumcoverfetcher_test.cpp) +add_test_file(xspfparser_test.cpp) diff --git a/tests/xspfparser_test.cpp b/tests/xspfparser_test.cpp new file mode 100644 index 000000000..89111456c --- /dev/null +++ b/tests/xspfparser_test.cpp @@ -0,0 +1,34 @@ +#include "test_utils.h" +#include "gtest/gtest.h" + +#include "xspfparser.h" + +#include + +class XSPFParserTest : public ::testing::Test { + +}; + +TEST_F(XSPFParserTest, ParsesOneTrackFromXML) { + QByteArray data = + "" + "http://example.com/foo.mp3" + "Foo" + "Bar" + "Baz" + "60000" + "http://example.com/albumcover.jpg" + "http://example.com" + ""; + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + XSPFParser parser(&buffer); + const SongList& songs = parser.Parse(); + ASSERT_EQ(1, songs.length()); + const Song& song = songs[0]; + EXPECT_EQ("Foo", song.title()); + EXPECT_EQ("Bar", song.artist()); + EXPECT_EQ("Baz", song.album()); + EXPECT_EQ("http://example.com/foo.mp3", song.filename()); + EXPECT_TRUE(song.is_valid()); +}