diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53c642a50..e104d9726 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,6 +41,7 @@ set(SOURCES core/scopedtransaction.cpp core/settingsprovider.cpp core/song.cpp + core/songloader.cpp core/stylesheetloader.cpp core/utilities.cpp @@ -151,6 +152,7 @@ set(HEADERS core/mergedproxymodel.h core/networkaccessmanager.h core/player.h + core/songloader.h engines/enginebase.h diff --git a/src/core/songloader.cpp b/src/core/songloader.cpp new file mode 100644 index 000000000..2f572bfb7 --- /dev/null +++ b/src/core/songloader.cpp @@ -0,0 +1,302 @@ +/* This file is part of Clementine. + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "songloader.h" +#include "core/song.h" +#include "playlistparsers/parserbase.h" +#include "playlistparsers/playlistparser.h" + +#include +#include +#include +#include +#include + +#include + +SongLoader::SongLoader(QObject *parent) + : QObject(parent), + timeout_timer_(new QTimer(this)), + playlist_parser_(new PlaylistParser(this)), + state_(WaitingForType), + success_(false), + parser_(NULL) +{ + timeout_timer_->setSingleShot(true); + connect(timeout_timer_, SIGNAL(timeout()), SLOT(Timeout())); +} + +SongLoader::Result SongLoader::Load(const QUrl& url, int timeout_msec) { + url_ = url; + + if (url_.scheme() == "file") { + return LoadLocal(); + } + + // TODO: Start timeout + return LoadRemote(); +} + +SongLoader::Result SongLoader::LoadLocal() { + // First check to see if it's a directory - if so we can load all the songs + // inside right away. + QString filename = url_.toLocalFile(); + if (QFileInfo(filename).isDir()) { + return LoadLocalDirectory(filename); + } + + // It's a local file, so check if it looks like a playlist. + // Read the first few bytes. + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) + return Error; + QByteArray data(file.read(PlaylistParser::kMagicSize)); + + ParserBase* parser = playlist_parser_->TryMagic(data); + if (parser) { + // It's a playlist! + file.seek(0); + songs_ = parser->Load(&file, QFileInfo(filename).path()); + } else { + // Not a playlist, so just assume it's a song + Song song; + song.InitFromFile(filename, -1); + songs_ << song; + } + + return Success; +} + +SongLoader::Result SongLoader::LoadLocalDirectory(const QString& filename) { + QDirIterator it(filename, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, + QDirIterator::Subdirectories); + + while (it.hasNext()) { + QString path = it.next(); + Song song; + song.InitFromFile(path, -1); + songs_ << song; + } + + return Success; +} + +SongLoader::Result SongLoader::LoadRemote() { + // It's not a local file so we have to fetch it to see what it is. We use + // gstreamer to do this since it handles funky URLs for us (http://, ssh://, + // etc) and also has typefinder plugins. + // First we wait for typefinder to tell us what it is. If it's not text/plain + // or text/uri-list assume it's a song and return success. + // Otherwise wait to get 512 bytes of data and do magic on it - if the magic + // fails then we don't no what it is so return failure. + // If the magic succeeds then we know for sure it's a playlist - so read the + // rest of the file, parse the playlist and return success. + + // Create the pipeline - it gets unreffed if it goes out of scope + boost::shared_ptr pipeline( + gst_pipeline_new(NULL), boost::bind(&gst_object_unref, _1)); + + // Create the source element automatically based on the URL + GstElement* source = gst_element_make_from_uri( + GST_URI_SRC, url_.toEncoded().constData(), NULL); + if (!source) { + qWarning() << "Couldn't create gstreamer source element for" << url_; + return Error; + } + + // Create the other elements and link them up + GstElement* typefind = gst_element_factory_make("typefind", NULL); + GstElement* fakesink = gst_element_factory_make("fakesink", NULL); + + gst_bin_add_many(GST_BIN(pipeline.get()), source, typefind, fakesink, NULL); + gst_element_link_many(source, typefind, fakesink, NULL); + + // Connect callbacks + GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline.get())); + g_signal_connect(typefind, "have-type", G_CALLBACK(TypeFound), this); + gst_bus_set_sync_handler(bus, BusCallbackSync, this); + gst_bus_add_watch(bus, BusCallback, this); + + // Add a probe to the sink so we can capture the data if it's a playlist + GstPad* pad = gst_element_get_pad(fakesink, "sink"); + gst_pad_add_buffer_probe(pad, G_CALLBACK(DataReady), this); + gst_object_unref(pad); + + // Start "playing" + gst_element_set_state(pipeline.get(), GST_STATE_PLAYING); + pipeline_ = pipeline; + return WillLoadAsync; +} + +void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) { + SongLoader* instance = static_cast(self); + + if (instance->state_ != WaitingForType) + return; + + // Check the mimetype + QString mimetype(gst_structure_get_name(gst_caps_get_structure(caps, 0))); + if (mimetype == "text/plain" || + mimetype == "text/uri-list") { + // Yeah it might be a playlist, let's get some data and have a better look + instance->state_ = WaitingForMagic; + return; + } + + // Nope, not a playlist - we're done + instance->StopTypefindAsync(true); +} + +void SongLoader::DataReady(GstPad *, GstBuffer *buf, void *self) { + SongLoader* instance = static_cast(self); + + if (instance->state_ == Finished) + return; + + // Append the data to the buffer + instance->buffer_.append(reinterpret_cast(GST_BUFFER_DATA(buf)), + GST_BUFFER_SIZE(buf)); + + if (instance->state_ == WaitingForMagic && + instance->buffer_.size() >= PlaylistParser::kMagicSize) { + // Got enough that we can test the magic + instance->MagicReady(); + } +} + +gboolean SongLoader::BusCallback(GstBus*, GstMessage* msg, gpointer self) { + SongLoader* instance = reinterpret_cast(self); + + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ERROR: + instance->ErrorMessageReceived(msg); + break; + + default: + break; + } + + gst_message_unref(msg); + return GST_BUS_DROP; +} + +GstBusSyncReply SongLoader::BusCallbackSync(GstBus*, GstMessage* msg, gpointer self) { + SongLoader* instance = reinterpret_cast(self); + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_EOS: + instance->EndOfStreamReached(); + break; + + case GST_MESSAGE_ERROR: + instance->ErrorMessageReceived(msg); + break; + + default: + break; + } + return GST_BUS_PASS; +} + +void SongLoader::ErrorMessageReceived(GstMessage* msg) { + if (state_ == Finished) + return; + + GError* error; + gchar* debugs; + + gst_message_parse_error(msg, &error, &debugs); + qDebug() << error->message; + qDebug() << debugs; + + g_error_free(error); + free(debugs); + + StopTypefindAsync(false); +} + +void SongLoader::EndOfStreamReached() { + switch (state_) { + case Finished: + break; + + case WaitingForMagic: + // Do the magic on the data we have already + MagicReady(); + if (state_ == Finished) + break; + // It looks like a playlist, so parse it + + // fallthrough + case WaitingForData: + // It's a playlist and we've got all the data - finish and parse it + StopTypefindAsync(true); + break; + + case WaitingForType: + StopTypefindAsync(false); + break; + } +} + +void SongLoader::MagicReady() { + parser_ = playlist_parser_->TryMagic(buffer_); + + if (!parser_) { + // It doesn't look like a playlist, so just finish + StopTypefindAsync(false); + return; + } + + // It is a playlist - we'll get more data and parse the whole thing in + // EndOfStreamReached + state_ = WaitingForData; +} + +void SongLoader::StopTypefindAsync(bool success) { + state_ = Finished; + success_ = success; + + metaObject()->invokeMethod(this, "StopTypefind", Qt::QueuedConnection); +} + +void SongLoader::StopTypefind() { + // Destroy the pipeline + if (pipeline_) { + gst_element_set_state(pipeline_.get(), GST_STATE_NULL); + pipeline_.reset(); + } + timeout_timer_->stop(); + + if (success_ && parser_) { + // Parse the playlist + QBuffer buf(&buffer_); + buf.open(QIODevice::ReadOnly); + songs_ = parser_->Load(&buf); + } else if (success_) { + // It wasn't a playlist - just put the URL in as a stream + Song song; + song.set_valid(true); + song.set_filetype(Song::Type_Stream); + song.set_title(url_.toString()); + songs_ << song; + } + + emit LoadFinished(success_); +} + +void SongLoader::Timeout() { + Q_ASSERT(0); // TODO +} diff --git a/src/core/songloader.h b/src/core/songloader.h new file mode 100644 index 000000000..f3a9aa40c --- /dev/null +++ b/src/core/songloader.h @@ -0,0 +1,92 @@ +/* This file is part of Clementine. + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef SONGLOADER_H +#define SONGLOADER_H + +#include +#include + +#include "song.h" + +#include +#include + +class ParserBase; +class PlaylistParser; + +class SongLoader : public QObject { + Q_OBJECT +public: + SongLoader(QObject* parent = 0); + + enum Result { + Success, + Error, + WillLoadAsync, + }; + + const QUrl& url() const { return url_; } + const SongList& songs() const { return songs_; } + + Result Load(const QUrl& url, int timeout_msec = 5000); + +signals: + void LoadFinished(bool success); + +private slots: + void Timeout(); + void StopTypefind(); + +private: + enum State { + WaitingForType, + WaitingForMagic, + WaitingForData, + Finished, + }; + + Result LoadLocal(); + Result LoadLocalDirectory(const QString& filename); + Result LoadRemote(); + + // GStreamer callbacks + static void TypeFound(GstElement* typefind, uint probability, GstCaps* caps, void* self); + static void DataReady(GstPad*, GstBuffer* buf, void* self); + static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage*, gpointer); + static gboolean BusCallback(GstBus*, GstMessage*, gpointer); + + void StopTypefindAsync(bool success); + void ErrorMessageReceived(GstMessage* msg); + void EndOfStreamReached(); + void MagicReady(); + +private: + QUrl url_; + SongList songs_; + + QTimer* timeout_timer_; + PlaylistParser* playlist_parser_; + + // For async loads + State state_; + bool success_; + boost::shared_ptr pipeline_; + ParserBase* parser_; + QByteArray buffer_; +}; + +#endif // SONGLOADER_H diff --git a/src/playlistparsers/asxparser.cpp b/src/playlistparsers/asxparser.cpp index f62734e28..63f3b1cc8 100644 --- a/src/playlistparsers/asxparser.cpp +++ b/src/playlistparsers/asxparser.cpp @@ -110,3 +110,7 @@ void ASXParser::Save(const SongList &songs, QIODevice *device, const QDir &dir) } writer.writeEndDocument(); } + +bool ASXParser::TryMagic(const QByteArray &data) const { + return data.toLower().contains("write("\n"); } } + +bool M3UParser::TryMagic(const QByteArray &data) const { + return data.contains("#EXTM3U") || data.contains("#EXTINF"); +} diff --git a/src/playlistparsers/m3uparser.h b/src/playlistparsers/m3uparser.h index c60bee458..cb41b834d 100644 --- a/src/playlistparsers/m3uparser.h +++ b/src/playlistparsers/m3uparser.h @@ -31,6 +31,9 @@ class M3UParser : public ParserBase { QString name() const { return "M3U"; } QStringList file_extensions() const { return QStringList() << "m3u"; } + QString mime_type() const { return "text/uri-list"; } + + bool TryMagic(const QByteArray &data) const; SongList Load(QIODevice* device, const QDir& dir = QDir()) const; void Save(const SongList &songs, QIODevice* device, const QDir& dir = QDir()) const; diff --git a/src/playlistparsers/parserbase.h b/src/playlistparsers/parserbase.h index 0068b5f2e..81c6c9cdf 100644 --- a/src/playlistparsers/parserbase.h +++ b/src/playlistparsers/parserbase.h @@ -30,6 +30,9 @@ public: virtual QString name() const = 0; virtual QStringList file_extensions() const = 0; + virtual QString mime_type() const { return QString(); } + + virtual bool TryMagic(const QByteArray& data) const = 0; virtual SongList Load(QIODevice* device, const QDir& dir = QDir()) const = 0; virtual void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const = 0; diff --git a/src/playlistparsers/playlistparser.cpp b/src/playlistparsers/playlistparser.cpp index 67b94a9f2..02bdc5c70 100644 --- a/src/playlistparsers/playlistparser.cpp +++ b/src/playlistparsers/playlistparser.cpp @@ -22,6 +22,8 @@ #include +const int PlaylistParser::kMagicSize = 512; + PlaylistParser::PlaylistParser(QObject *parent) : QObject(parent) { @@ -60,10 +62,6 @@ QString PlaylistParser::filters() const { return filters.join(";;"); } -bool PlaylistParser::can_load(const QString &filename) const { - return file_extensions().contains(QFileInfo(filename).suffix()); -} - ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const { foreach (ParserBase* p, parsers_) { if (p->file_extensions().contains(suffix)) @@ -72,11 +70,23 @@ ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const { return NULL; } -SongList PlaylistParser::Load(const QString &filename) const { +ParserBase* PlaylistParser::ParserForData(const QByteArray &data) const { + foreach (ParserBase* p, parsers_) { + if (p->TryMagic(data)) + return p; + } + return NULL; +} + +ParserBase* PlaylistParser::TryMagic(const QByteArray &data) const { + return ParserForData(data); +} + +SongList PlaylistParser::Load(const QString &filename, ParserBase* p) const { QFileInfo info(filename); // Find a parser that supports this file extension - ParserBase* parser = ParserForExtension(info.suffix()); + ParserBase* parser = p ? p : ParserForExtension(info.suffix()); if (!parser) { qWarning() << "Unknown filetype:" << filename; return SongList(); diff --git a/src/playlistparsers/playlistparser.h b/src/playlistparsers/playlistparser.h index 855cc53ec..c5a46340b 100644 --- a/src/playlistparsers/playlistparser.h +++ b/src/playlistparsers/playlistparser.h @@ -29,16 +29,19 @@ class PlaylistParser : public QObject { public: PlaylistParser(QObject *parent = 0); + static const int kMagicSize; + QStringList file_extensions() const; QString filters() const; - bool can_load(const QString& filename) const; + ParserBase* TryMagic(const QByteArray& data) const; - SongList Load(const QString& filename) const; + SongList Load(const QString& filename, ParserBase* parser = 0) const; void Save(const SongList& songs, const QString& filename) const; private: ParserBase* ParserForExtension(const QString& suffix) const; + ParserBase* ParserForData(const QByteArray& data) const; QList parsers_; }; diff --git a/src/playlistparsers/plsparser.cpp b/src/playlistparsers/plsparser.cpp index 9486566e5..9c663a78a 100644 --- a/src/playlistparsers/plsparser.cpp +++ b/src/playlistparsers/plsparser.cpp @@ -94,3 +94,7 @@ void PLSParser::Save(const SongList &songs, QIODevice *device, const QDir &dir) temp_file.seek(0); device->write(temp_file.readAll()); } + +bool PLSParser::TryMagic(const QByteArray &data) const { + return data.contains("[playlist]"); +} diff --git a/src/playlistparsers/plsparser.h b/src/playlistparsers/plsparser.h index 8675023d4..3f3405bd7 100644 --- a/src/playlistparsers/plsparser.h +++ b/src/playlistparsers/plsparser.h @@ -28,6 +28,8 @@ public: QString name() const { return "PLS"; } QStringList file_extensions() const { return QStringList() << "pls"; } + bool TryMagic(const QByteArray &data) const; + SongList Load(QIODevice* device, const QDir& dir = QDir()) const; void Save(const SongList& songs, QIODevice* device, const QDir& dir = QDir()) const; }; diff --git a/src/playlistparsers/xspfparser.cpp b/src/playlistparsers/xspfparser.cpp index efa92ebf9..dca0df8dc 100644 --- a/src/playlistparsers/xspfparser.cpp +++ b/src/playlistparsers/xspfparser.cpp @@ -137,3 +137,8 @@ void XSPFParser::Save(const SongList &songs, QIODevice *device, const QDir &dir) } writer.writeEndDocument(); } + +bool XSPFParser::TryMagic(const QByteArray &data) const { + QByteArray lower(data.toLower()); + return lower.contains(" urls; + /*QList urls; foreach (const QString& path, file_names) { if (playlist_parser_->can_load(path)) { playlists_->current()->InsertSongs(playlist_parser_->Load(path)); @@ -1052,7 +1052,8 @@ void MainWindow::AddFile() { urls << url; } } - playlists_->current()->InsertPaths(urls); + playlists_->current()->InsertPaths(urls);*/ + // TODO: Fix } void MainWindow::AddFolder() { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9fc16ea54..ab922fb5a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -105,3 +105,4 @@ add_test_file(fileformats_test.cpp false) add_test_file(mergedproxymodel_test.cpp false) add_test_file(plsparser_test.cpp false) add_test_file(asxparser_test.cpp false) +add_test_file(songloader_test.cpp false) diff --git a/tests/data/test.asx b/tests/data/test.asx new file mode 100644 index 000000000..e0f2c9383 --- /dev/null +++ b/tests/data/test.asx @@ -0,0 +1,6 @@ +foobar + + Foo + Bar + mumble mumble + diff --git a/tests/data/test.xspf b/tests/data/test.xspf new file mode 100644 index 000000000..f965e920e --- /dev/null +++ b/tests/data/test.xspf @@ -0,0 +1,9 @@ + + http://example.com/foo.mp3 + Foo + Bar + Baz + 60000 + http://example.com/albumcover.jpg + http://example.com + diff --git a/tests/data/testdata.qrc b/tests/data/testdata.qrc index 261b2a64a..7b8c6d4a6 100644 --- a/tests/data/testdata.qrc +++ b/tests/data/testdata.qrc @@ -10,5 +10,7 @@ pls_one.pls pls_somafm.pls test.m3u + test.xspf + test.asx diff --git a/tests/metatypes_env.h b/tests/metatypes_env.h index 73f195a63..b5fd05c8a 100644 --- a/tests/metatypes_env.h +++ b/tests/metatypes_env.h @@ -23,6 +23,7 @@ #include #include "core/song.h" +#include "core/songloader.h" #include "library/directory.h" class MetatypesEnvironment : public ::testing::Environment { @@ -34,6 +35,7 @@ public: qRegisterMetaType("SubdirectoryList"); qRegisterMetaType("SongList"); qRegisterMetaType("QModelIndex"); + qRegisterMetaType("SongLoader::Result"); } }; diff --git a/tests/songloader_test.cpp b/tests/songloader_test.cpp new file mode 100644 index 000000000..79231a359 --- /dev/null +++ b/tests/songloader_test.cpp @@ -0,0 +1,177 @@ +/* This file is part of Clementine. + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "test_utils.h" +#include "gmock/gmock-matchers.h" +#include "gtest/gtest.h" + +#include "core/songloader.h" +#include "engines/gstengine.h" + +#include +#include +#include + +#include + +class SongLoaderTest : public ::testing::Test { +public: + static void SetUpTestCase() { + sGstEngine = new GstEngine; + ASSERT_TRUE(sGstEngine->Init()); + } + + static void TearDownTestCase() { + delete sGstEngine; + sGstEngine = NULL; + } + +protected: + void SetUp() { + loader_.reset(new SongLoader); + } + + static const char* kRemoteUrl; + static GstEngine* sGstEngine; + + boost::scoped_ptr loader_; +}; + +const char* SongLoaderTest::kRemoteUrl = "http://remotetestdata.clementine-player.org"; +GstEngine* SongLoaderTest::sGstEngine = NULL; + +TEST_F(SongLoaderTest, LoadLocalMp3) { + TemporaryResource file(":/testdata/beep.mp3"); + SongLoader::Result ret = loader_->Load(QUrl::fromLocalFile(file.fileName())); + + ASSERT_EQ(SongLoader::Success, ret); + ASSERT_EQ(1, loader_->songs().count()); + EXPECT_TRUE(loader_->songs()[0].is_valid()); + EXPECT_EQ("Beep mp3", loader_->songs()[0].title()); +} + +TEST_F(SongLoaderTest, LoadLocalPls) { + TemporaryResource file(":/testdata/pls_one.pls"); + SongLoader::Result ret = loader_->Load(QUrl::fromLocalFile(file.fileName())); + + ASSERT_EQ(SongLoader::Success, ret); + ASSERT_EQ(1, loader_->songs().count()); + EXPECT_EQ("Title", loader_->songs()[0].title()); + EXPECT_EQ(123, loader_->songs()[0].length()); +} + +TEST_F(SongLoaderTest, LoadLocalM3U) { + TemporaryResource file(":/testdata/test.m3u"); + SongLoader::Result ret = loader_->Load(QUrl::fromLocalFile(file.fileName())); + + ASSERT_EQ(SongLoader::Success, ret); + ASSERT_EQ(239, loader_->songs().count()); +} + +TEST_F(SongLoaderTest, LoadLocalXSPF) { + TemporaryResource file(":/testdata/test.xspf"); + SongLoader::Result ret = loader_->Load(QUrl::fromLocalFile(file.fileName())); + + ASSERT_EQ(SongLoader::Success, ret); + ASSERT_EQ(1, loader_->songs().count()); + EXPECT_EQ("Foo", loader_->songs()[0].title()); +} + +TEST_F(SongLoaderTest, LoadLocalASX) { + TemporaryResource file(":/testdata/test.asx"); + SongLoader::Result ret = loader_->Load(QUrl::fromLocalFile(file.fileName())); + + ASSERT_EQ(SongLoader::Success, ret); + ASSERT_EQ(1, loader_->songs().count()); + EXPECT_EQ("Foo", loader_->songs()[0].title()); +} + +TEST_F(SongLoaderTest, LoadRemoteMp3) { + SongLoader::Result ret = loader_->Load(QString(kRemoteUrl) + "/beep.mp3"); + ASSERT_EQ(SongLoader::WillLoadAsync, ret); + + QSignalSpy spy(loader_.get(), SIGNAL(LoadFinished(bool))); + + // Start an event loop to wait for gstreamer to do its thing + QEventLoop loop; + QObject::connect(loader_.get(), SIGNAL(LoadFinished(bool)), + &loop, SLOT(quit())); + loop.exec(); + + // Check the signal was emitted with Success + ASSERT_EQ(1, spy.count()); + EXPECT_EQ(true, spy[0][0].toBool()); + + // Check the song got loaded + ASSERT_EQ(1, loader_->songs().count()); +} + +TEST_F(SongLoaderTest, LoadRemote404) { + SongLoader::Result ret = loader_->Load(QString(kRemoteUrl) + "/404.mp3"); + ASSERT_EQ(SongLoader::WillLoadAsync, ret); + + QSignalSpy spy(loader_.get(), SIGNAL(LoadFinished(bool))); + + // Start an event loop to wait for gstreamer to do its thing + QEventLoop loop; + QObject::connect(loader_.get(), SIGNAL(LoadFinished(bool)), + &loop, SLOT(quit())); + loop.exec(); + + // Check the signal was emitted with Error + ASSERT_EQ(1, spy.count()); + EXPECT_EQ(false, spy[0][0].toBool()); +} + +TEST_F(SongLoaderTest, LoadRemotePls) { + SongLoader::Result ret = loader_->Load(QString(kRemoteUrl) + "/pls_somafm.pls"); + ASSERT_EQ(SongLoader::WillLoadAsync, ret); + + QSignalSpy spy(loader_.get(), SIGNAL(LoadFinished(bool))); + + // Start an event loop to wait for gstreamer to do its thing + QEventLoop loop; + QObject::connect(loader_.get(), SIGNAL(LoadFinished(bool)), + &loop, SLOT(quit())); + loop.exec(); + + // Check the signal was emitted with Success + ASSERT_EQ(1, spy.count()); + EXPECT_EQ(true, spy[0][0].toBool()); + + // Check some metadata + ASSERT_EQ(4, loader_->songs().count()); + EXPECT_EQ("SomaFM: Groove Salad (#3 128k mp3): A nicely chilled plate of ambient beats and grooves.", + loader_->songs()[2].title()); + EXPECT_EQ("http://ice.somafm.com/groovesalad", loader_->songs()[3].filename()); +} + +TEST_F(SongLoaderTest, LoadRemotePlainText) { + SongLoader::Result ret = loader_->Load(QString(kRemoteUrl) + "/notaplaylist.txt"); + ASSERT_EQ(SongLoader::WillLoadAsync, ret); + + QSignalSpy spy(loader_.get(), SIGNAL(LoadFinished(bool))); + + // Start an event loop to wait for gstreamer to do its thing + QEventLoop loop; + QObject::connect(loader_.get(), SIGNAL(LoadFinished(bool)), + &loop, SLOT(quit())); + loop.exec(); + + // Check the signal was emitted with Error + ASSERT_EQ(1, spy.count()); + EXPECT_EQ(false, spy[0][0].toBool()); +} diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 9310e3dc0..eed82058f 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -16,6 +16,7 @@ #include "test_utils.h" +#include #include #include #include @@ -47,3 +48,15 @@ void PrintTo(const ::QString& str, std::ostream& os) { void PrintTo(const ::QVariant& var, std::ostream& os) { os << var.toString().toStdString(); } + +TemporaryResource::TemporaryResource(const QString& filename) { + setFileTemplate(QDir::tempPath() + "/clementine_test-XXXXXX." + + filename.section('.', -1, -1)); + open(); + + QFile resource(filename); + resource.open(QIODevice::ReadOnly); + write(resource.readAll()); + + seek(0); +} diff --git a/tests/test_utils.h b/tests/test_utils.h index 84e5f586f..b833a0fa9 100644 --- a/tests/test_utils.h +++ b/tests/test_utils.h @@ -21,6 +21,7 @@ #include #include +#include class QNetworkRequest; class QString; @@ -44,4 +45,9 @@ void PrintTo(const ::QVariant& var, std::ostream& os); Q_DECLARE_METATYPE(QModelIndex); +class TemporaryResource : public QTemporaryFile { +public: + TemporaryResource(const QString& filename); +}; + #endif // TEST_UTILS_H