The start of some code to automagically determine whether a URL is a playlist or a song, and load the songs in the playlist if it's a playlist. Still to do: timeout, forcing M3U for text/uri-list.

This commit is contained in:
David Sansome 2010-06-15 13:24:17 +00:00
parent 73a381fe89
commit 08a01d6997
23 changed files with 665 additions and 10 deletions

View File

@ -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

302
src/core/songloader.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "songloader.h"
#include "core/song.h"
#include "playlistparsers/parserbase.h"
#include "playlistparsers/playlistparser.h"
#include <QBuffer>
#include <QDirIterator>
#include <QFileInfo>
#include <QTimer>
#include <QtDebug>
#include <boost/bind.hpp>
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<GstElement> 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<SongLoader*>(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<SongLoader*>(self);
if (instance->state_ == Finished)
return;
// Append the data to the buffer
instance->buffer_.append(reinterpret_cast<const char*>(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<SongLoader*>(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<SongLoader*>(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
}

92
src/core/songloader.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef SONGLOADER_H
#define SONGLOADER_H
#include <QObject>
#include <QUrl>
#include "song.h"
#include <boost/shared_ptr.hpp>
#include <gst/gst.h>
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<GstElement> pipeline_;
ParserBase* parser_;
QByteArray buffer_;
};
#endif // SONGLOADER_H

View File

@ -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("<asx");
}

View File

@ -28,6 +28,8 @@ class ASXParser : public XMLParser {
QString name() const { return "ASX"; }
QStringList file_extensions() const { return QStringList() << "asx"; }
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;

View File

@ -106,3 +106,7 @@ void M3UParser::Save(const SongList &songs, QIODevice *device, const QDir &dir)
device->write("\n");
}
}
bool M3UParser::TryMagic(const QByteArray &data) const {
return data.contains("#EXTM3U") || data.contains("#EXTINF");
}

View File

@ -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;

View File

@ -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;

View File

@ -22,6 +22,8 @@
#include <QtDebug>
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();

View File

@ -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<ParserBase*> parsers_;
};

View File

@ -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]");
}

View File

@ -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;
};

View File

@ -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("<playlist") && lower.contains("<tracklist");
}

View File

@ -33,6 +33,8 @@ class XSPFParser : public XMLParser {
QString name() const { return "XSPF"; }
QStringList file_extensions() const { return QStringList() << "xspf"; }
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;

View File

@ -1041,7 +1041,7 @@ void MainWindow::AddFile() {
settings_.setValue("add_media_path", file_names[0]);
// Add media
QList<QUrl> urls;
/*QList<QUrl> 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() {

View File

@ -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)

6
tests/data/test.asx Normal file
View File

@ -0,0 +1,6 @@
<asx version="3.0"><title>foobar</title><entry>
<ref href="http://example.com/foo.mp3"/>
<title>Foo</title>
<author>Bar</author>
<copyright>mumble mumble</copyright>
</entry></asx>

9
tests/data/test.xspf Normal file
View File

@ -0,0 +1,9 @@
<playlist><trackList><track>
<location>http://example.com/foo.mp3</location>
<title>Foo</title>
<creator>Bar</creator>
<album>Baz</album>
<duration>60000</duration>
<image>http://example.com/albumcover.jpg</image>
<info>http://example.com</info>
</track></trackList></playlist>

View File

@ -10,5 +10,7 @@
<file>pls_one.pls</file>
<file>pls_somafm.pls</file>
<file>test.m3u</file>
<file>test.xspf</file>
<file>test.asx</file>
</qresource>
</RCC>

View File

@ -23,6 +23,7 @@
#include <QModelIndex>
#include "core/song.h"
#include "core/songloader.h"
#include "library/directory.h"
class MetatypesEnvironment : public ::testing::Environment {
@ -34,6 +35,7 @@ public:
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
qRegisterMetaType<SongList>("SongList");
qRegisterMetaType<QModelIndex>("QModelIndex");
qRegisterMetaType<SongLoader::Result>("SongLoader::Result");
}
};

177
tests/songloader_test.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#include "test_utils.h"
#include "gmock/gmock-matchers.h"
#include "gtest/gtest.h"
#include "core/songloader.h"
#include "engines/gstengine.h"
#include <QBuffer>
#include <QEventLoop>
#include <QSignalSpy>
#include <boost/scoped_ptr.hpp>
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<SongLoader> 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());
}

View File

@ -16,6 +16,7 @@
#include "test_utils.h"
#include <QDir>
#include <QNetworkRequest>
#include <QString>
#include <QUrl>
@ -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);
}

View File

@ -21,6 +21,7 @@
#include <QMetaType>
#include <QModelIndex>
#include <QTemporaryFile>
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