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/scopedtransaction.cpp
|
||||||
core/settingsprovider.cpp
|
core/settingsprovider.cpp
|
||||||
core/song.cpp
|
core/song.cpp
|
||||||
|
core/songloader.cpp
|
||||||
core/stylesheetloader.cpp
|
core/stylesheetloader.cpp
|
||||||
core/utilities.cpp
|
core/utilities.cpp
|
||||||
|
|
||||||
@ -151,6 +152,7 @@ set(HEADERS
|
|||||||
core/mergedproxymodel.h
|
core/mergedproxymodel.h
|
||||||
core/networkaccessmanager.h
|
core/networkaccessmanager.h
|
||||||
core/player.h
|
core/player.h
|
||||||
|
core/songloader.h
|
||||||
|
|
||||||
engines/enginebase.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();
|
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"; }
|
QString name() const { return "ASX"; }
|
||||||
QStringList file_extensions() const { return QStringList() << "asx"; }
|
QStringList file_extensions() const { return QStringList() << "asx"; }
|
||||||
|
|
||||||
|
bool TryMagic(const QByteArray &data) const;
|
||||||
|
|
||||||
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
|
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
|
||||||
void Save(const SongList &songs, 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");
|
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"; }
|
QString name() const { return "M3U"; }
|
||||||
QStringList file_extensions() const { return QStringList() << "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;
|
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
|
||||||
void Save(const SongList &songs, 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 QString name() const = 0;
|
||||||
virtual QStringList file_extensions() 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 SongList Load(QIODevice* device, const QDir& dir = QDir()) const = 0;
|
||||||
virtual void Save(const SongList& songs, 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>
|
#include <QtDebug>
|
||||||
|
|
||||||
|
const int PlaylistParser::kMagicSize = 512;
|
||||||
|
|
||||||
PlaylistParser::PlaylistParser(QObject *parent)
|
PlaylistParser::PlaylistParser(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
@ -60,10 +62,6 @@ QString PlaylistParser::filters() const {
|
|||||||
return filters.join(";;");
|
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 {
|
ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const {
|
||||||
foreach (ParserBase* p, parsers_) {
|
foreach (ParserBase* p, parsers_) {
|
||||||
if (p->file_extensions().contains(suffix))
|
if (p->file_extensions().contains(suffix))
|
||||||
@ -72,11 +70,23 @@ ParserBase* PlaylistParser::ParserForExtension(const QString& suffix) const {
|
|||||||
return NULL;
|
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);
|
QFileInfo info(filename);
|
||||||
|
|
||||||
// Find a parser that supports this file extension
|
// Find a parser that supports this file extension
|
||||||
ParserBase* parser = ParserForExtension(info.suffix());
|
ParserBase* parser = p ? p : ParserForExtension(info.suffix());
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
qWarning() << "Unknown filetype:" << filename;
|
qWarning() << "Unknown filetype:" << filename;
|
||||||
return SongList();
|
return SongList();
|
||||||
|
@ -29,16 +29,19 @@ class PlaylistParser : public QObject {
|
|||||||
public:
|
public:
|
||||||
PlaylistParser(QObject *parent = 0);
|
PlaylistParser(QObject *parent = 0);
|
||||||
|
|
||||||
|
static const int kMagicSize;
|
||||||
|
|
||||||
QStringList file_extensions() const;
|
QStringList file_extensions() const;
|
||||||
QString filters() 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;
|
void Save(const SongList& songs, const QString& filename) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ParserBase* ParserForExtension(const QString& suffix) const;
|
ParserBase* ParserForExtension(const QString& suffix) const;
|
||||||
|
ParserBase* ParserForData(const QByteArray& data) const;
|
||||||
|
|
||||||
QList<ParserBase*> parsers_;
|
QList<ParserBase*> parsers_;
|
||||||
};
|
};
|
||||||
|
@ -94,3 +94,7 @@ void PLSParser::Save(const SongList &songs, QIODevice *device, const QDir &dir)
|
|||||||
temp_file.seek(0);
|
temp_file.seek(0);
|
||||||
device->write(temp_file.readAll());
|
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"; }
|
QString name() const { return "PLS"; }
|
||||||
QStringList file_extensions() const { return QStringList() << "pls"; }
|
QStringList file_extensions() const { return QStringList() << "pls"; }
|
||||||
|
|
||||||
|
bool TryMagic(const QByteArray &data) const;
|
||||||
|
|
||||||
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
|
SongList Load(QIODevice* device, const QDir& dir = QDir()) const;
|
||||||
void Save(const SongList& songs, 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();
|
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"; }
|
QString name() const { return "XSPF"; }
|
||||||
QStringList file_extensions() const { return QStringList() << "xspf"; }
|
QStringList file_extensions() const { return QStringList() << "xspf"; }
|
||||||
|
|
||||||
|
bool TryMagic(const QByteArray &data) const;
|
||||||
|
|
||||||
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
|
SongList Load(QIODevice *device, const QDir &dir = QDir()) const;
|
||||||
void Save(const SongList &songs, 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]);
|
settings_.setValue("add_media_path", file_names[0]);
|
||||||
|
|
||||||
// Add media
|
// Add media
|
||||||
QList<QUrl> urls;
|
/*QList<QUrl> urls;
|
||||||
foreach (const QString& path, file_names) {
|
foreach (const QString& path, file_names) {
|
||||||
if (playlist_parser_->can_load(path)) {
|
if (playlist_parser_->can_load(path)) {
|
||||||
playlists_->current()->InsertSongs(playlist_parser_->Load(path));
|
playlists_->current()->InsertSongs(playlist_parser_->Load(path));
|
||||||
@ -1052,7 +1052,8 @@ void MainWindow::AddFile() {
|
|||||||
urls << url;
|
urls << url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
playlists_->current()->InsertPaths(urls);
|
playlists_->current()->InsertPaths(urls);*/
|
||||||
|
// TODO: Fix
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::AddFolder() {
|
void MainWindow::AddFolder() {
|
||||||
|
@ -105,3 +105,4 @@ add_test_file(fileformats_test.cpp false)
|
|||||||
add_test_file(mergedproxymodel_test.cpp false)
|
add_test_file(mergedproxymodel_test.cpp false)
|
||||||
add_test_file(plsparser_test.cpp false)
|
add_test_file(plsparser_test.cpp false)
|
||||||
add_test_file(asxparser_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_one.pls</file>
|
||||||
<file>pls_somafm.pls</file>
|
<file>pls_somafm.pls</file>
|
||||||
<file>test.m3u</file>
|
<file>test.m3u</file>
|
||||||
|
<file>test.xspf</file>
|
||||||
|
<file>test.asx</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
#include <QModelIndex>
|
#include <QModelIndex>
|
||||||
|
|
||||||
#include "core/song.h"
|
#include "core/song.h"
|
||||||
|
#include "core/songloader.h"
|
||||||
#include "library/directory.h"
|
#include "library/directory.h"
|
||||||
|
|
||||||
class MetatypesEnvironment : public ::testing::Environment {
|
class MetatypesEnvironment : public ::testing::Environment {
|
||||||
@ -34,6 +35,7 @@ public:
|
|||||||
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
|
qRegisterMetaType<SubdirectoryList>("SubdirectoryList");
|
||||||
qRegisterMetaType<SongList>("SongList");
|
qRegisterMetaType<SongList>("SongList");
|
||||||
qRegisterMetaType<QModelIndex>("QModelIndex");
|
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 "test_utils.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
@ -47,3 +48,15 @@ void PrintTo(const ::QString& str, std::ostream& os) {
|
|||||||
void PrintTo(const ::QVariant& var, std::ostream& os) {
|
void PrintTo(const ::QVariant& var, std::ostream& os) {
|
||||||
os << var.toString().toStdString();
|
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 <QMetaType>
|
||||||
#include <QModelIndex>
|
#include <QModelIndex>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
|
||||||
class QNetworkRequest;
|
class QNetworkRequest;
|
||||||
class QString;
|
class QString;
|
||||||
@ -44,4 +45,9 @@ void PrintTo(const ::QVariant& var, std::ostream& os);
|
|||||||
|
|
||||||
Q_DECLARE_METATYPE(QModelIndex);
|
Q_DECLARE_METATYPE(QModelIndex);
|
||||||
|
|
||||||
|
class TemporaryResource : public QTemporaryFile {
|
||||||
|
public:
|
||||||
|
TemporaryResource(const QString& filename);
|
||||||
|
};
|
||||||
|
|
||||||
#endif // TEST_UTILS_H
|
#endif // TEST_UTILS_H
|
||||||
|
Loading…
x
Reference in New Issue
Block a user