Commit 3cf74c35 authored by Michael Pyne's avatar Michael Pyne

Move initial music load to a separate thread.

I ran into all the problems one might expect from adding threading to an
old codebase but this variant seems pretty stable.

The benefit is that the heavy I/O with large music libraries is now off
of the GUI thread. So even though it will still take awhile to load all
music until I fix the bug(s) with using cached tags, at least the
application itself will be responsive while it loads audio.
parent cd9f45c1
......@@ -81,6 +81,7 @@ set(juk_SRCS ${tunepimp_SRCS}
dbuscollectionproxy.cpp
deletedialog.cpp
directorylist.cpp
directoryloader.cpp
dynamicplaylist.cpp
exampleoptions.cpp
folderplaylist.cpp
......
/**
* Copyright (C) 2018 Michael Pyne <mpyne@kde.org>
*
* This program 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 2 of the License, or (at your option) any later
* version.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "directoryloader.h"
#include <QList> // for FileHandleList
#include <QFileInfo>
#include "mediafiles.h"
// Classifies files into types for potential loading purposes.
enum class MediaFileType {
UnusableFile,
MediaFile,
Playlist,
Directory
};
static MediaFileType classifyFile(const QFileInfo &fileInfo);
static FileHandle loadMediaFile(const QString &fileName);
DirectoryLoader::DirectoryLoader(const QString &dir, QObject *parent)
: QObject(parent)
, m_dirIterator(
dir,
QDir::AllEntries | QDir::NoDotAndDotDot,
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks)
{
}
void DirectoryLoader::startLoading()
{
static const int BATCH_SIZE = 256;
FileHandleList files;
while(m_dirIterator.hasNext()) {
const auto fileName = m_dirIterator.next();
const QFileInfo fileInfo(fileName);
const auto type = classifyFile(fileInfo);
switch(type) {
case MediaFileType::Playlist:
emit loadedPlaylist(fileName);
break;
case MediaFileType::MediaFile:
{
const auto loadedMetadata = loadMediaFile(fileInfo.canonicalFilePath());
files << loadedMetadata;
if(files.count() >= BATCH_SIZE) {
emit loadedFiles(files);
files.clear();
}
}
break;
case MediaFileType::Directory:
// this should be impossible based on the
// QDirIterator settings
continue;
case MediaFileType::UnusableFile:
continue;
default:
continue;
}
}
if(!files.isEmpty()) {
emit loadedFiles(files);
}
emit doneLoading();
}
MediaFileType classifyFile(const QFileInfo &fileInfo)
{
const QString path = fileInfo.canonicalFilePath();
if(fileInfo.isFile() && fileInfo.isReadable() &&
MediaFiles::isMediaFile(path))
{
return MediaFileType::MediaFile;
}
// These are all the files we care about, anything remaining needs to be a
// directory or playlist or it must be unusable
if(MediaFiles::isPlaylistFile(path)) {
return MediaFileType::Playlist;
}
if(fileInfo.isDir()) {
return MediaFileType::Directory;
}
return MediaFileType::UnusableFile;
}
FileHandle loadMediaFile(const QString &fileName)
{
FileHandle loadedMetadata(fileName);
(void) loadedMetadata.tag(); // Ensure tag is read
return loadedMetadata;
}
/**
* Copyright (C) 2018 Michael Pyne <mpyne@kde.org>
*
* This program 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 2 of the License, or (at your option) any later
* version.
*
* This program 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
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef JUK_DIRECTORYLOADER_H
#define JUK_DIRECTORYLOADER_H
#include <QObject>
#include <QDirIterator>
#include "filehandle.h"
/**
* Loads music files and their metadata from a given directory, emitting loaded
* files in a batch periodically. Intended for use in a separate thread as a
* worker object.
*/
class DirectoryLoader : public QObject {
Q_OBJECT
public:
DirectoryLoader(const QString &dir, QObject *parent = nullptr);
public slots:
void startLoading();
signals:
void doneLoading();
void loadedFiles(FileHandleList files);
void loadedPlaylist(QString fileName);
private:
QDirIterator m_dirIterator;
};
#endif // JUK_DIRECTORYLOADER_H
......@@ -57,7 +57,7 @@ public:
QFileInfo fileInfo;
QString absFilePath;
QDateTime baseModificationTime;
QDateTime lastModified;
mutable QDateTime lastModified;
};
////////////////////////////////////////////////////////////////////////////////
......
......@@ -18,6 +18,8 @@
#define JUK_FILEHANDLE_H
#include <QExplicitlySharedDataPointer>
#include <QList>
#include <QMetaType>
class QString;
class QFileInfo;
......@@ -29,9 +31,6 @@ class CoverInfo;
class Tag;
class CacheDataStream;
template<class T>
class QList;
/**
* A value based, explicitly shared wrapper around file related information
* used in JuK's playlists.
......@@ -80,6 +79,9 @@ private:
typedef QList<FileHandle> FileHandleList;
Q_DECLARE_METATYPE(FileHandle);
Q_DECLARE_METATYPE(FileHandleList);
QDataStream &operator<<(QDataStream &s, const FileHandle &f);
CacheDataStream &operator>>(CacheDataStream &s, FileHandle &f);
......
......@@ -36,6 +36,7 @@
#include <QDir>
#include <QDirIterator>
#include <QMenuBar>
#include <QMetaType>
#include <QTime>
#include <QTimer>
#include <QDesktopWidget>
......@@ -88,6 +89,10 @@ JuK::JuK(const QStringList &filesToOpen, QWidget *parent) :
{
// Expect segfaults if you change this order.
// Allow to be passed across threads
qRegisterMetaType<FileHandle>();
qRegisterMetaType<FileHandleList>();
m_instance = this;
readSettings();
......
......@@ -54,6 +54,7 @@
#include <QStackedWidget>
#include <QScrollBar>
#include <QPainter>
#include <QThread>
#include <id3v1genres.h>
......@@ -61,6 +62,7 @@
#include <cmath>
#include <algorithm>
#include "directoryloader.h"
#include "playlistitem.h"
#include "playlistcollection.h"
#include "playlistsearch.h"
......@@ -1142,8 +1144,9 @@ void Playlist::addFiles(const QStringList &files, PlaylistItem *after)
FileHandleList queue;
foreach(const QString &file, files)
addFile(file, queue, true, &after);
for(const auto &file : files) {
addUntypedFile(file, queue, true, &after);
}
addFileHelper(queue, &after, true);
......@@ -1601,7 +1604,25 @@ void Playlist::calculateColumnWeights()
m_weightDirty.clear();
}
void Playlist::addFile(const QString &file, FileHandleList &files, bool importPlaylists,
void Playlist::addPlaylistFile(const QString &m3uFile)
{
if (!m_collection->containsPlaylistFile(m3uFile)) {
new Playlist(m_collection, QFileInfo(m3uFile));
}
}
/**
* Super spaghetti function that adds music files, m3u playlist files, or directories
* into the playlist as appropriate, but only if the playlist doesn't already contain
* the file.
*
* @p file is the file to add (music, playlist or directory)
* @p files is the current batch of FileHandles to add into this playlist
* (maintained by addFileHelper)
* @p after is a pointer to a PlaylistItem* which itself points to the item to
* insert after (maintained by addFileHelper)
*/
void Playlist::addUntypedFile(const QString &file, FileHandleList &files, bool importPlaylists,
PlaylistItem **after)
{
if(hasItem(file) && !m_allowDuplicates)
......@@ -1609,36 +1630,20 @@ void Playlist::addFile(const QString &file, FileHandleList &files, bool importPl
addFileHelper(files, after);
// Our biggest thing that we're fighting during startup is too many stats
// of files. Make sure that we don't do one here if it's not needed.
const CollectionListItem *item = CollectionList::instance()->lookup(file);
if(item && !item->file().isNull()) {
FileHandle cached(item->file());
cached.tag();
files.append(cached);
return;
}
const QFileInfo fileInfo(QDir::cleanPath(file));
if(!fileInfo.exists())
return;
const QFileInfo fileInfo(file);
const QString canonicalPath = fileInfo.canonicalFilePath();
if(fileInfo.isFile() && fileInfo.isReadable()) {
if(MediaFiles::isMediaFile(file)) {
FileHandle f(fileInfo);
f.tag();
files.append(f);
}
if(fileInfo.isFile() && fileInfo.isReadable() &&
MediaFiles::isMediaFile(file))
{
FileHandle f(fileInfo);
f.tag();
files.append(f);
return;
}
if(importPlaylists && MediaFiles::isPlaylistFile(file) &&
!m_collection->containsPlaylistFile(canonicalPath))
{
new Playlist(m_collection, fileInfo);
if(importPlaylists && MediaFiles::isPlaylistFile(file)) {
addPlaylistFile(canonicalPath);
return;
}
......@@ -1648,17 +1653,34 @@ void Playlist::addFile(const QString &file, FileHandleList &files, bool importPl
return; // Exclude it
}
QDirIterator dirIterator(canonicalPath, QDir::AllEntries | QDir::NoDotAndDotDot);
DirectoryLoader *loader = new DirectoryLoader(canonicalPath);
QThread *loaderThread = new QThread;
while(dirIterator.hasNext()) {
// We set importPlaylists to the value from the add directories
// dialog as we want to load all of the ones that the user has
// explicitly asked for, but not those that we find in lower
// directories.
loader->moveToThread(loaderThread);
addFile(dirIterator.next(), files,
m_collection->importPlaylists(), after);
}
connect(loaderThread, &QThread::started, loader, &DirectoryLoader::startLoading);
connect(loader, &DirectoryLoader::doneLoading, loaderThread, &QThread::quit);
connect(loader, &DirectoryLoader::doneLoading, loader, &QObject::deleteLater);
connect(loaderThread, &QThread::finished, loaderThread, &QObject::deleteLater);
connect(loader, &DirectoryLoader::loadedPlaylist, this,
[this](const QString &m3uFile) {
addPlaylistFile(m3uFile);
}
);
connect(loader, &DirectoryLoader::loadedFiles, this,
[this](const FileHandleList &newFiles) {
// NOTE: after and files are both invalid by this point since
// this can be called long after our own caller has returned.
PlaylistItem *after = nullptr;
for(const auto newFile : newFiles) {
after = createItem(newFile, after);
}
}
);
loaderThread->start();
}
}
......
......@@ -538,8 +538,9 @@ private:
*/
void calculateColumnWeights();
void addFile(const QString &file, FileHandleList &files, bool importPlaylists,
PlaylistItem **after);
void addPlaylistFile(const QString &m3uFile);
void addUntypedFile(const QString &file, FileHandleList &files, bool importPlaylists,
PlaylistItem **after);
void addFileHelper(FileHandleList &files, PlaylistItem **after,
bool ignoreTimer = false);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment