Create or load moodbar data for songs

This commit is contained in:
David Sansome 2012-05-25 17:18:07 +01:00
parent 10a3594590
commit 51229b88c5
11 changed files with 549 additions and 1 deletions

View File

@ -198,6 +198,10 @@ set(SOURCES
library/librarywatcher.cpp
library/sqlrow.cpp
moodbar/moodbarcontroller.cpp
moodbar/moodbarloader.cpp
moodbar/moodbarpipeline.cpp
musicbrainz/acoustidclient.cpp
musicbrainz/chromaprinter.cpp
musicbrainz/musicbrainzclient.cpp
@ -459,6 +463,10 @@ set(HEADERS
library/libraryviewcontainer.h
library/librarywatcher.h
moodbar/moodbarcontroller.h
moodbar/moodbarloader.h
moodbar/moodbarpipeline.h
musicbrainz/acoustidclient.h
musicbrainz/musicbrainzclient.h
musicbrainz/tagfetcher.h

View File

@ -29,6 +29,8 @@
#include "globalsearch/globalsearch.h"
#include "library/library.h"
#include "library/librarybackend.h"
#include "moodbar/moodbarcontroller.h"
#include "moodbar/moodbarloader.h"
#include "playlist/playlistbackend.h"
#include "playlist/playlistmanager.h"
#include "podcasts/gpoddersync.h"
@ -55,7 +57,9 @@ Application::Application(QObject* parent)
device_manager_(NULL),
podcast_updater_(NULL),
podcast_downloader_(NULL),
gpodder_sync_(NULL)
gpodder_sync_(NULL),
moodbar_loader_(NULL),
moodbar_controller_(NULL)
{
tag_reader_client_ = new TagReaderClient(this);
MoveToNewThread(tag_reader_client_);
@ -86,6 +90,8 @@ Application::Application(QObject* parent)
podcast_updater_ = new PodcastUpdater(this, this);
podcast_downloader_ = new PodcastDownloader(this, this);
gpodder_sync_ = new GPodderSync(this, this);
moodbar_loader_ = new MoodbarLoader(this);
moodbar_controller_ = new MoodbarController(this, this);
library_->Init();
library_->StartThreads();

View File

@ -34,6 +34,8 @@ class InternetModel;
class Library;
class LibraryBackend;
class LibraryModel;
class MoodbarController;
class MoodbarLoader;
class Player;
class PlaylistBackend;
class PodcastDownloader;
@ -69,6 +71,8 @@ public:
PodcastUpdater* podcast_updater() const { return podcast_updater_; }
PodcastDownloader* podcast_downloader() const { return podcast_downloader_; }
GPodderSync* gpodder_sync() const { return gpodder_sync_; }
MoodbarLoader* moodbar_loader() const { return moodbar_loader_; }
MoodbarController* moodbar_controller() const { return moodbar_controller_; }
LibraryBackend* library_backend() const;
LibraryModel* library_model() const;
@ -105,6 +109,8 @@ private:
PodcastUpdater* podcast_updater_;
PodcastDownloader* podcast_downloader_;
GPodderSync* gpodder_sync_;
MoodbarLoader* moodbar_loader_;
MoodbarController* moodbar_controller_;
QList<QObject*> objects_in_threads_;
QList<QThread*> threads_;

View File

@ -277,6 +277,9 @@ QString GetConfigPath(ConfigPath config) {
case Path_NetworkCache:
return GetConfigPath(Path_Root) + "/networkcache";
case Path_MoodbarCache:
return GetConfigPath(Path_Root) + "/moodbarcache";
case Path_GstreamerRegistry:
return GetConfigPath(Path_Root) +

View File

@ -108,6 +108,7 @@ namespace Utilities {
Path_GstreamerRegistry,
Path_DefaultMusicLibrary,
Path_LocalSpotifyBlob,
Path_MoodbarCache,
};
QString GetConfigPath(ConfigPath config);

View File

@ -0,0 +1,36 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 "moodbarcontroller.h"
#include "moodbarloader.h"
#include "core/application.h"
#include "core/logging.h"
#include "playlist/playlistmanager.h"
MoodbarController::MoodbarController(Application* app, QObject* parent)
: QObject(parent),
app_(app)
{
connect(app_->playlist_manager(),
SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
}
void MoodbarController::CurrentSongChanged(const Song& song) {
QByteArray data;
MoodbarPipeline* pipeline = NULL;
app_->moodbar_loader()->Load(song.url(), &data, &pipeline);
}

View File

@ -0,0 +1,39 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 MOODBARCONTROLLER_H
#define MOODBARCONTROLLER_H
#include <QObject>
class Application;
class Song;
class MoodbarController : public QObject {
Q_OBJECT
public:
MoodbarController(Application* app, QObject* parent = 0);
private slots:
void CurrentSongChanged(const Song& song);
private:
Application* app_;
};
#endif // MOODBARCONTROLLER_H

View File

@ -0,0 +1,117 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 "moodbarloader.h"
#include "moodbarpipeline.h"
#include "core/closure.h"
#include "core/utilities.h"
#include <QDir>
#include <QFileInfo>
#include <QNetworkDiskCache>
#include <QUrl>
MoodbarLoader::MoodbarLoader(QObject* parent)
: QObject(parent),
cache_(new QNetworkDiskCache(this))
{
cache_->setCacheDirectory(Utilities::GetConfigPath(Utilities::Path_MoodbarCache));
cache_->setMaximumCacheSize(1024 * 1024); // 1MB - enough for 333 moodbars
}
MoodbarLoader::~MoodbarLoader() {
}
MoodbarLoader::Result MoodbarLoader::Load(
const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline) {
if (url.scheme() != "file") {
return CannotLoad;
}
// Are we in the middle of loading this moodbar already?
if (active_requests_.contains(url)) {
*async_pipeline = active_requests_[url];
return WillLoadAsync;
}
// Check if a mood file exists for this file already
const QString filename(url.toLocalFile());
const QFileInfo file_info(filename);
const QString dir_path(file_info.dir().path());
QStringList parts(file_info.fileName().split('.'));
parts.removeLast();
parts.append("mood");
const QString mood_filename(parts.join("."));
QStringList possible_mood_files;
possible_mood_files << dir_path + "/." + mood_filename
<< dir_path + "/" + mood_filename;
foreach (const QString& possible_mood_file, possible_mood_files) {
QFile f(possible_mood_file);
if (f.open(QIODevice::ReadOnly)) {
qLog(Info) << "Loading moodbar data from" << possible_mood_file;
*data = f.readAll();
return Loaded;
}
}
// Maybe it exists in the cache?
QIODevice* cache_device = cache_->data(url);
if (cache_device) {
qLog(Info) << "Loading cached moodbar data for" << filename;
*data = cache_device->readAll();
delete cache_device;
return Loaded;
}
// There was no existing file, analyze the audio file and create one.
MoodbarPipeline* pipeline = new MoodbarPipeline(filename);
if (!pipeline->Start()) {
delete pipeline;
return CannotLoad;
}
qLog(Info) << "Creating moodbar data for" << filename;
active_requests_[filename] = pipeline;
NewClosure(pipeline, SIGNAL(Finished(bool)),
this, SLOT(RequestFinished(MoodbarPipeline*,QUrl)),
pipeline, url);
*async_pipeline = pipeline;
return WillLoadAsync;
}
void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
if (request->success()) {
qLog(Info) << "Moodbar data generated successfully for" << url.toLocalFile();
// Save the data in the cache
QNetworkCacheMetaData metadata;
metadata.setUrl(url);
QIODevice* cache_file = cache_->prepare(metadata);
cache_file->write(request->data());
cache_->insert(cache_file);
}
// Remove the request from the active list and delete it
active_requests_.take(url);
request->deleteLater();
}

View File

@ -0,0 +1,60 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 MOODBARLOADER_H
#define MOODBARLOADER_H
#include <QMap>
#include <QObject>
class QNetworkDiskCache;
class QUrl;
class MoodbarPipeline;
class MoodbarLoader : public QObject {
Q_OBJECT
public:
MoodbarLoader(QObject* parent = 0);
~MoodbarLoader();
enum Result {
// The URL isn't a local file or the moodbar plugin was not available -
// moodbar data can never be loaded.
CannotLoad,
// Moodbar data was loaded and returned.
Loaded,
// Moodbar data will be loaded in the background, a MoodbarPipeline* was
// was returned that you can connect to the Finished() signal on.
WillLoadAsync
};
Result Load(const QUrl& url, QByteArray* data, MoodbarPipeline** async_pipeline);
private slots:
void RequestFinished(MoodbarPipeline* request, const QUrl& filename);
private:
QNetworkDiskCache* cache_;
QMap<QUrl, MoodbarPipeline*> active_requests_;
};
#endif // MOODBARLOADER_H

View File

@ -0,0 +1,202 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 "moodbarpipeline.h"
#include "core/logging.h"
bool MoodbarPipeline::sIsAvailable = false;
MoodbarPipeline::MoodbarPipeline(const QString& local_filename)
: QObject(NULL),
local_filename_(local_filename),
pipeline_(NULL),
convert_element_(NULL),
bus_callback_id_(0),
success_(false)
{
}
MoodbarPipeline::~MoodbarPipeline() {
Cleanup();
}
bool MoodbarPipeline::IsAvailable() {
if (!sIsAvailable) {
GstElementFactory* factory = gst_element_factory_find("fftwspectrum");
if (!factory) {
return false;
}
gst_object_unref(factory);
factory = gst_element_factory_find("moodbar");
if (!factory) {
return false;
}
gst_object_unref(factory);
sIsAvailable = true;
}
return sIsAvailable;
}
GstElement* MoodbarPipeline::CreateElement(const QString& factory_name) {
GstElement* ret = gst_element_factory_make(factory_name.toAscii().constData(), NULL);
if (ret) {
gst_bin_add(GST_BIN(pipeline_), ret);
} else {
qLog(Warning) << "Unable to create gstreamer element" << factory_name;
}
return ret;
}
bool MoodbarPipeline::Start() {
if (pipeline_) {
return false;
}
pipeline_ = gst_pipeline_new("moodbar-pipeline");
GstElement* filesrc = CreateElement("filesrc");
GstElement* decodebin = CreateElement("decodebin");
convert_element_ = CreateElement("audioconvert");
GstElement* fftwspectrum = CreateElement("fftwspectrum");
GstElement* moodbar = CreateElement("moodbar");
GstElement* appsink = CreateElement("appsink");
if (!filesrc || !convert_element_ || !fftwspectrum || !moodbar || !appsink) {
pipeline_ = NULL;
return false;
}
// Join them together
gst_element_link(filesrc, decodebin);
gst_element_link_many(convert_element_, fftwspectrum, moodbar, appsink, NULL);
// Set properties
g_object_set(filesrc, "location", local_filename_.toUtf8().constData(), NULL);
g_object_set(fftwspectrum, "def-size", 2048,
"def-step", 1024,
"hiquality", true, NULL);
g_object_set(moodbar, "height", 1,
"max-width", 1000, NULL);
// Connect signals
g_signal_connect(decodebin, "new-decoded-pad", G_CALLBACK(NewPadCallback), this);
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallbackSync, this);
bus_callback_id_ = gst_bus_add_watch(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), BusCallback, this);
// Set appsink callbacks
GstAppSinkCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.new_buffer = NewBufferCallback;
gst_app_sink_set_callbacks(reinterpret_cast<GstAppSink*>(appsink), &callbacks, this, NULL);
// Start playing
gst_element_set_state(pipeline_, GST_STATE_PLAYING);
return true;
}
void MoodbarPipeline::ReportError(GstMessage* msg) {
GError* error;
gchar* debugs;
gst_message_parse_error(msg, &error, &debugs);
QString message = QString::fromLocal8Bit(error->message);
g_error_free(error);
free(debugs);
qLog(Error) << "Error processing" << local_filename_ << ":" << message;
}
void MoodbarPipeline::NewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer data) {
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
GstPad* const audiopad = gst_element_get_pad(self->convert_element_, "sink");
if (GST_PAD_IS_LINKED(audiopad)) {
qLog(Warning) << "audiopad is already linked, unlinking old pad";
gst_pad_unlink(audiopad, GST_PAD_PEER(audiopad));
}
gst_pad_link(pad, audiopad);
gst_object_unref(audiopad);
}
GstFlowReturn MoodbarPipeline::NewBufferCallback(GstAppSink* app_sink, gpointer data) {
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
GstBuffer* buffer = gst_app_sink_pull_buffer(app_sink);
self->data_.append(reinterpret_cast<const char*>(buffer->data), buffer->size);
gst_buffer_unref(buffer);
return GST_FLOW_OK;
}
gboolean MoodbarPipeline::BusCallback(GstBus*, GstMessage* msg, gpointer data) {
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_ERROR:
self->ReportError(msg);
self->Stop(false);
break;
default:
break;
}
return GST_BUS_DROP;
}
GstBusSyncReply MoodbarPipeline::BusCallbackSync(GstBus*, GstMessage* msg, gpointer data) {
MoodbarPipeline* self = reinterpret_cast<MoodbarPipeline*>(data);
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_EOS:
self->Stop(true);
break;
case GST_MESSAGE_ERROR:
self->ReportError(msg);
self->Stop(false);
break;
default:
break;
}
return GST_BUS_PASS;
}
void MoodbarPipeline::Stop(bool success) {
success_ = success;
emit Finished(success);
Cleanup();
}
void MoodbarPipeline::Cleanup() {
if (pipeline_) {
gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(pipeline_)), NULL, NULL);
g_source_remove(bus_callback_id_);
gst_object_unref(pipeline_);
pipeline_ = NULL;
}
}

View File

@ -0,0 +1,70 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
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 MOODBARPIPELINE_H
#define MOODBARPIPELINE_H
#include <QBuffer>
#include <QObject>
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
// Creates moodbar data for a single local music file.
class MoodbarPipeline : public QObject {
Q_OBJECT
public:
MoodbarPipeline(const QString& local_filename);
~MoodbarPipeline();
static bool IsAvailable();
bool Start();
bool success() const { return success_; }
const QByteArray& data() const { return data_; }
signals:
void Finished(bool success);
private:
GstElement* CreateElement(const QString& factory_name);
void ReportError(GstMessage* message);
void Stop(bool success);
void Cleanup();
static void NewPadCallback(GstElement*, GstPad* pad, gboolean, gpointer data);
static GstFlowReturn NewBufferCallback(GstAppSink* app_sink, gpointer self);
static gboolean BusCallback(GstBus*, GstMessage* msg, gpointer data);
static GstBusSyncReply BusCallbackSync(GstBus*, GstMessage* msg, gpointer data);
private:
static bool sIsAvailable;
QString local_filename_;
GstElement* pipeline_;
GstElement* convert_element_;
guint bus_callback_id_;
bool success_;
QByteArray data_;
};
#endif // MOODBARPIPELINE_H