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:
parent
73a381fe89
commit
08a01d6997
@ -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
302
src/core/songloader.cpp
Normal 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
92
src/core/songloader.h
Normal 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
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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_;
|
||||
};
|
||||
|
@ -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]");
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
6
tests/data/test.asx
Normal 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
9
tests/data/test.xspf
Normal 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>
|
@ -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>
|
||||
|
@ -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
177
tests/songloader_test.cpp
Normal 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());
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user