Load all songs that require disc-read non-blocking.

This commit is contained in:
Alexander Bikadorov 2014-04-02 15:57:01 +02:00
parent a471789a59
commit 0d199be5a7
6 changed files with 129 additions and 190 deletions

View File

@ -17,7 +17,6 @@
#include "songloader.h"
#include <functional>
#include <memory>
#include <QBuffer>
@ -113,8 +112,14 @@ SongLoader::Result SongLoader::Load(const QUrl& url) {
url_ = PodcastUrlLoader::FixPodcastUrl(url_);
timeout_timer_->start(timeout_);
return LoadRemote();
preload_func_ = std::bind(&SongLoader::LoadRemote, this);
return WillLoadAsync;
}
void SongLoader::PreLoad() {
if (preload_func_) {
preload_func_();
}
}
SongLoader::Result SongLoader::LoadLocalPartial(const QString& filename) {
@ -244,95 +249,82 @@ void SongLoader::AudioCDTagsLoaded(
SongLoader::Result SongLoader::LoadLocal(const QString& filename) {
qLog(Debug) << "Loading local file" << filename;
// First check to see if it's a directory - if so we can load all the songs
// inside right away.
if (QFileInfo(filename).isDir()) {
ConcurrentRun::Run<void>(
&thread_pool_,
std::bind(&SongLoader::LoadLocalDirectoryAndEmit, this, filename));
return WillLoadAsync;
}
// 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_->ParserForMagic(data);
if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a
// basic M3U file which is just a plain list of filenames.
parser = playlist_parser_->ParserForExtension(QFileInfo(filename).suffix());
}
if (parser) {
qLog(Debug) << "Parsing using" << parser->name();
// It's a playlist!
ConcurrentRun::Run<void>(
&thread_pool_,
std::bind(&SongLoader::LoadPlaylistAndEmit, this, parser, filename));
return WillLoadAsync;
}
// Not a playlist, so just assume it's a song
// Search in the database.
QUrl url = QUrl::fromLocalFile(filename);
LibraryQuery query;
query.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec);
query.AddWhere("filename", url.toEncoded());
SongList song_list;
if (library_->ExecQuery(&query) && query.Next()) {
// we may have many results when the file has many sections
do {
Song song;
song.InitFromQuery(query, true);
song_list << song;
if (song.is_valid()) {
songs_ << song;
}
} while (query.Next());
} else {
QString matching_cue = filename.section('.', 0, -2) + ".cue";
if (QFile::exists(matching_cue)) {
// it's a cue - create virtual tracks
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
song_list = cue_parser_->Load(&cue, matching_cue,
QDir(filename.section('/', 0, -2)));
} else {
// it's a normal media file, load it asynchronously.
TagReaderReply* reply = TagReaderClient::Instance()->ReadFile(filename);
NewClosure(reply, SIGNAL(Finished(bool)), this,
SLOT(LocalFileLoaded(TagReaderReply*)), reply);
return WillLoadAsync;
}
return Success;
}
for (const Song& song : song_list) {
if (song.is_valid()) {
songs_ << song;
}
}
return Success;
// It's not in the database, load it asynchronously.
preload_func_ =
std::bind(&SongLoader::LoadLocalAsync, this, filename);
return WillLoadAsync;
}
void SongLoader::LocalFileLoaded(TagReaderReply* reply) {
reply->deleteLater();
void SongLoader::LoadLocalAsync(const QString& filename) {
Song song;
song.InitFromProtobuf(reply->message().read_file_response().metadata());
if (song.is_valid()) {
songs_ << song;
// First check to see if it's a directory - if so we will load all the songs
// inside right away.
if (QFileInfo(filename).isDir()) {
LoadLocalDirectory(filename);
return;
}
emit LoadFinished(true);
// 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;
QByteArray data(file.read(PlaylistParser::kMagicSize));
ParserBase* parser = playlist_parser_->ParserForMagic(data);
if (!parser) {
// Check the file extension as well, maybe the magic failed, or it was a
// basic M3U file which is just a plain list of filenames.
parser = playlist_parser_->ParserForExtension(QFileInfo(filename).suffix().toLower());
}
if (parser) {
qLog(Debug) << "Parsing using" << parser->name();
// It's a playlist!
LoadPlaylist(parser, filename);
return;
}
// Check if it's a cue file
QString matching_cue = filename.section('.', 0, -2) + ".cue";
if (QFile::exists(matching_cue)) {
// it's a cue - create virtual tracks
QFile cue(matching_cue);
cue.open(QIODevice::ReadOnly);
SongList song_list = cue_parser_->Load(&cue, matching_cue,
QDir(filename.section('/', 0, -2)));
for (Song song: song_list){
if (song.is_valid()) songs_ << song;
}
return;
}
// Assume it's just a normal file
Song song;
song.InitFromFilePartial(filename);
if (song.is_valid()) songs_ << song;
}
void SongLoader::EffectiveSongsLoad() {
@ -360,12 +352,6 @@ void SongLoader::EffectiveSongLoad(Song* song) {
}
}
void SongLoader::LoadPlaylistAndEmit(ParserBase* parser,
const QString& filename) {
LoadPlaylist(parser, filename);
emit LoadFinished(true);
}
void SongLoader::LoadPlaylist(ParserBase* parser, const QString& filename) {
QFile file(filename);
file.open(QIODevice::ReadOnly);
@ -385,11 +371,6 @@ static bool CompareSongs(const Song& left, const Song& right) {
return left.url() < right.url();
}
void SongLoader::LoadLocalDirectoryAndEmit(const QString& filename) {
LoadLocalDirectory(filename);
emit LoadFinished(true);
}
void SongLoader::LoadLocalDirectory(const QString& filename) {
QDirIterator it(filename, QDir::Files | QDir::NoDotAndDotDot | QDir::Readable,
QDirIterator::Subdirectories);
@ -460,7 +441,7 @@ void SongLoader::StopTypefind() {
emit LoadFinished(success_);
}
SongLoader::Result SongLoader::LoadRemote() {
void SongLoader::LoadRemote() {
qLog(Debug) << "Loading remote file" << url_;
// It's not a local file so we have to fetch it to see what it is. We use
@ -473,6 +454,8 @@ SongLoader::Result SongLoader::LoadRemote() {
// 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.
timeout_timer_->start(timeout_);
// Create the pipeline - it gets unreffed if it goes out of scope
std::shared_ptr<GstElement> pipeline(gst_pipeline_new(nullptr),
std::bind(&gst_object_unref, _1));
@ -483,7 +466,7 @@ SongLoader::Result SongLoader::LoadRemote() {
if (!source) {
qLog(Warning) << "Couldn't create gstreamer source element for"
<< url_.toString();
return Error;
return;
}
// Create the other elements and link them up
@ -505,10 +488,15 @@ SongLoader::Result SongLoader::LoadRemote() {
gst_pad_add_buffer_probe(pad, G_CALLBACK(DataReady), this);
gst_object_unref(pad);
QEventLoop loop;
loop.connect(this, SIGNAL(LoadFinished(bool)), SLOT(quit()));
// Start "playing"
gst_element_set_state(pipeline.get(), GST_STATE_PLAYING);
pipeline_ = pipeline;
return WillLoadAsync;
// Wait until loading is finished
loop.exec();
}
void SongLoader::TypeFound(GstElement*, uint, GstCaps* caps, void* self) {

View File

@ -18,6 +18,7 @@
#ifndef SONGLOADER_H
#define SONGLOADER_H
#include <functional>
#include <memory>
#include <gst/gst.h>
@ -54,15 +55,12 @@ class SongLoader : public QObject {
int timeout() const { return timeout_; }
void set_timeout(int msec) { timeout_ = msec; }
// If Success is returned the songs are fully loaded. If WillLoadAsync is
// returned PreLoad() needs to be called next.
Result Load(const QUrl& url);
// To effectively load the songs:
// when we call Load() on a directory, it will return WillLoadAsync, load the
// files with only filenames and emit LoadFinished(). When LoadFinished() is
// received by songloaderinserter, it will insert songs (incompletely loaded)
// in playlist, and call EffectiveSongsLoad() in a background thread to
// perform the real load of the songs. Next, UpdateItems() will be called on
// playlist and replace the partially-loaded items by the new ones, fully
// loaded.
// Loads the files with only filenames (blocking) and emits LoadFinished().
void PreLoad();
// Completely load songs (blocking) previously loaded with PreLoad().
void EffectiveSongsLoad();
void EffectiveSongLoad(Song* song);
Result LoadAudioCD();
@ -75,21 +73,19 @@ signals:
void StopTypefind();
void AudioCDTagsLoaded(const QString& artist, const QString& album,
const MusicBrainzClient::ResultList& results);
void LocalFileLoaded(TagReaderReply* reply);
private:
enum State { WaitingForType, WaitingForMagic, WaitingForData, Finished, };
Result LoadLocal(const QString& filename);
void LoadLocalAsync(const QString& filename);
Result LoadLocalPartial(const QString& filename);
void LoadLocalDirectory(const QString& filename);
void LoadPlaylist(ParserBase* parser, const QString& filename);
void LoadLocalDirectoryAndEmit(const QString& filename);
void LoadPlaylistAndEmit(ParserBase* parser, const QString& filename);
void AddAsRawStream();
Result LoadRemote();
void LoadRemote();
// GStreamer callbacks
static void TypeFound(GstElement* typefind, uint probability, GstCaps* caps,
@ -116,6 +112,7 @@ signals:
CueParser* cue_parser_;
// For async loads
std::function<void()> preload_func_;
int timeout_;
State state_;
bool success_;

View File

@ -158,37 +158,16 @@ void PlaylistManager::New(const QString& name, const SongList& songs,
}
void PlaylistManager::Load(const QString& filename) {
QUrl url = QUrl::fromLocalFile(filename);
SongLoader* loader = new SongLoader(library_backend_, app_->player(), this);
connect(loader, SIGNAL(LoadFinished(bool)), SLOT(LoadFinished(bool)));
SongLoader::Result result = loader->Load(url);
QFileInfo info(filename);
if (result == SongLoader::Error ||
(result == SongLoader::Success && loader->songs().isEmpty())) {
app_->AddError(tr("The playlist '%1' was empty or could not be loaded.")
.arg(info.completeBaseName()));
delete loader;
return;
}
int id = playlist_backend_->CreatePlaylist(info.baseName(), QString());
if (result == SongLoader::Success) {
New(info.baseName(), loader->songs());
delete loader;
}
}
if (id == -1) qFatal("Couldn't create playlist");
void PlaylistManager::LoadFinished(bool success) {
SongLoader* loader = qobject_cast<SongLoader*>(sender());
loader->deleteLater();
QString localfile = loader->url().toLocalFile();
QFileInfo info(localfile);
if (!success || loader->songs().isEmpty()) {
app_->AddError(tr("The playlist '%1' was empty or could not be loaded.")
.arg(info.completeBaseName()));
}
Playlist* playlist = AddPlaylist(id, info.baseName(), QString(), QString(), false);
New(info.baseName(), loader->songs());
QList<QUrl> urls;
playlist->InsertUrls(urls << QUrl::fromLocalFile(filename));
}
void PlaylistManager::Save(int id, const QString& filename) {

View File

@ -229,7 +229,6 @@ class PlaylistManager : public PlaylistManagerInterface {
void OneOfPlaylistsChanged();
void UpdateSummaryText();
void SongsDiscovered(const SongList& songs);
void LoadFinished(bool success);
void ItemsLoadedForSavePlaylist(QFutureWatcher<Song>* watcher,
const QString& filename);

View File

@ -31,15 +31,10 @@ SongLoaderInserter::SongLoaderInserter(TaskManager* task_manager,
row_(-1),
play_now_(true),
enqueue_(false),
async_load_id_(0),
async_progress_(0),
library_(library),
player_(player) {}
SongLoaderInserter::~SongLoaderInserter() {
qDeleteAll(pending_);
qDeleteAll(pending_async_);
}
SongLoaderInserter::~SongLoaderInserter() { qDeleteAll(pending_); }
void SongLoaderInserter::Load(Playlist* destination, int row, bool play_now,
bool enqueue, const QList<QUrl>& urls) {
@ -49,20 +44,17 @@ void SongLoaderInserter::Load(Playlist* destination, int row, bool play_now,
enqueue_ = enqueue;
connect(destination, SIGNAL(destroyed()), SLOT(DestinationDestroyed()));
connect(this, SIGNAL(PreloadFinished()), SLOT(InsertSongs()));
connect(this, SIGNAL(EffectiveLoadFinished(const SongList&)), destination,
SLOT(UpdateItems(const SongList&)));
for (const QUrl& url : urls) {
SongLoader* loader = new SongLoader(library_, player_, this);
// we're connecting this before we're even sure if this is an async load
// to avoid race conditions (signal emission before we're listening to it)
connect(loader, SIGNAL(LoadFinished(bool)),
SLOT(PendingLoadFinished(bool)));
SongLoader::Result ret = loader->Load(url);
if (ret == SongLoader::WillLoadAsync) {
pending_.insert(loader);
pending_.append(loader);
continue;
}
@ -73,19 +65,17 @@ void SongLoaderInserter::Load(Playlist* destination, int row, bool play_now,
delete loader;
}
if (pending_.isEmpty())
Finished();
else {
async_progress_ = 0;
async_load_id_ = task_manager_->StartTask(tr("Loading tracks"));
task_manager_->SetTaskProgress(async_load_id_, async_progress_,
pending_.count());
if (pending_.isEmpty()) {
InsertSongs();
deleteLater();
} else {
QtConcurrent::run(this, &SongLoaderInserter::AsyncLoad);
}
}
// Load audio CD tracks:
// First, we add tracks (without metadata) into the playlist
// In the meantine, MusicBrainz will be queried to get songs' metadata.
// In the meantime, MusicBrainz will be queried to get songs' metadata.
// AudioCDTagsLoaded will be called next, and playlist's items will be updated.
void SongLoaderInserter::LoadAudioCD(Playlist* destination, int row,
bool play_now, bool enqueue) {
@ -101,9 +91,10 @@ void SongLoaderInserter::LoadAudioCD(Playlist* destination, int row,
if (ret == SongLoader::Error) {
emit Error(tr("Error while loading audio CD"));
delete loader;
} else {
songs_ = loader->songs();
InsertSongs();
}
songs_ = loader->songs();
PartiallyFinished();
}
void SongLoaderInserter::DestinationDestroyed() { destination_ = nullptr; }
@ -119,30 +110,7 @@ void SongLoaderInserter::AudioCDTagsLoaded(bool success) {
deleteLater();
}
void SongLoaderInserter::PendingLoadFinished(bool success) {
SongLoader* loader = qobject_cast<SongLoader*>(sender());
if (!loader || !pending_.contains(loader)) return;
pending_.remove(loader);
pending_async_.insert(loader);
if (success)
songs_ << loader->songs();
else
emit Error(tr("Error loading %1").arg(loader->url().toString()));
task_manager_->SetTaskProgress(async_load_id_, ++async_progress_);
if (pending_.isEmpty()) {
task_manager_->SetTaskFinished(async_load_id_);
async_progress_ = 0;
async_load_id_ = task_manager_->StartTask(tr("Loading tracks info"));
task_manager_->SetTaskProgress(async_load_id_, async_progress_,
pending_async_.count());
PartiallyFinished();
QtConcurrent::run(this, &SongLoaderInserter::EffectiveLoad);
}
}
void SongLoaderInserter::PartiallyFinished() {
void SongLoaderInserter::InsertSongs() {
// Insert songs (that haven't been completelly loaded) to allow user to see
// and play them while not loaded completely
if (destination_) {
@ -150,23 +118,35 @@ void SongLoaderInserter::PartiallyFinished() {
}
}
void SongLoaderInserter::EffectiveLoad() {
void SongLoaderInserter::AsyncLoad() {
// First, quick load raw songs.
int async_progress = 0;
int async_load_id = task_manager_->StartTask(tr("Loading tracks"));
task_manager_->SetTaskProgress(async_load_id, async_progress,
pending_.count());
for (SongLoader* loader : pending_) {
loader->PreLoad();
task_manager_->SetTaskProgress(async_load_id, ++async_progress);
songs_ << loader->songs();
}
task_manager_->SetTaskFinished(async_load_id);
emit PreloadFinished();
// Songs are inserted in playlist, now load them completely.
async_progress = 0;
async_load_id = task_manager_->StartTask(tr("Loading tracks info"));
task_manager_->SetTaskProgress(async_load_id, async_progress, songs_.count());
SongList songs;
for (SongLoader* loader : pending_async_) {
for (SongLoader* loader : pending_) {
loader->EffectiveSongsLoad();
task_manager_->SetTaskProgress(async_load_id_, ++async_progress_);
songs << loader->songs();
task_manager_->SetTaskProgress(async_load_id, songs.count());
}
task_manager_->SetTaskFinished(async_load_id);
// Replace the partially-loaded items by the new ones, fully loaded.
emit EffectiveLoadFinished(songs);
task_manager_->SetTaskFinished(async_load_id_);
deleteLater();
}
void SongLoaderInserter::Finished() {
if (destination_) {
destination_->InsertSongsOrLibraryItems(songs_, row_, play_now_, enqueue_);
}
deleteLater();
}

View File

@ -18,8 +18,8 @@
#ifndef SONGLOADERINSERTER_H
#define SONGLOADERINSERTER_H
#include <QList>
#include <QObject>
#include <QSet>
#include <QUrl>
#include "core/song.h"
@ -45,17 +45,16 @@ class SongLoaderInserter : public QObject {
signals:
void Error(const QString& message);
void PreloadFinished();
void EffectiveLoadFinished(const SongList& songs);
private slots:
void PendingLoadFinished(bool success);
void DestinationDestroyed();
void AudioCDTagsLoaded(bool success);
void InsertSongs();
private:
void PartiallyFinished();
void EffectiveLoad();
void Finished();
void AsyncLoad();
private:
TaskManager* task_manager_;
@ -67,10 +66,7 @@ signals:
SongList songs_;
QSet<SongLoader*> pending_;
QSet<SongLoader*> pending_async_;
int async_load_id_;
int async_progress_;
QList<SongLoader*> pending_;
LibraryBackendInterface* library_;
const Player* player_;
};