Big refactoring of the Library <-> LibraryBackend <-> SQLite interaction.
The LibraryBackend has now been split into a Database class that deals with setting up sqlite, and PlaylistBackend that contains the functions for persisting the playlist. The LibraryBackend now only contains functions for accessing "a collection of songs", and can be parameterised with table names to access different collections. It also no longer lives in a background thread, and follows the Qt memory management model instead of using shared_ptr. Most of Library has been moved into LibraryModel - a QAbstractItemModel for any LibraryBackend. What's left of Library is now specific to the user's local library on disk.
This commit is contained in:
parent
cd8fc47bf3
commit
1b00aaa8b3
@ -81,6 +81,9 @@ set(CLEMENTINE-SOURCES
|
||||
transcoderformats.cpp
|
||||
transcodedialog.cpp
|
||||
magnatuneservice.cpp
|
||||
database.cpp
|
||||
librarymodel.cpp
|
||||
playlistbackend.cpp
|
||||
)
|
||||
|
||||
# Header files that have Q_OBJECT in
|
||||
@ -147,6 +150,9 @@ set(CLEMENTINE-MOC-HEADERS
|
||||
transcoder.h
|
||||
transcodedialog.h
|
||||
magnatuneservice.h
|
||||
librarymodel.h
|
||||
playlistbackend.h
|
||||
database.h
|
||||
)
|
||||
|
||||
# lists of engine source files
|
||||
|
@ -35,9 +35,11 @@
|
||||
|
||||
const char* AlbumCoverManager::kSettingsGroup = "CoverManager";
|
||||
|
||||
AlbumCoverManager::AlbumCoverManager(QNetworkAccessManager* network, QWidget *parent)
|
||||
AlbumCoverManager::AlbumCoverManager(QNetworkAccessManager* network,
|
||||
LibraryBackend* backend, QWidget *parent)
|
||||
: QDialog(parent),
|
||||
constructed_(false),
|
||||
backend_(backend),
|
||||
cover_loader_(new BackgroundThreadImplementation<AlbumCoverLoader, AlbumCoverLoader>(this)),
|
||||
cover_fetcher_(new AlbumCoverFetcher(network, this)),
|
||||
artist_icon_(":/artist.png"),
|
||||
@ -119,13 +121,6 @@ void AlbumCoverManager::CoverLoaderInitialised() {
|
||||
SLOT(CoverImageLoaded(quint64,QImage)));
|
||||
}
|
||||
|
||||
void AlbumCoverManager::SetBackend(boost::shared_ptr<LibraryBackendInterface> backend) {
|
||||
backend_ = backend;
|
||||
|
||||
if (isVisible())
|
||||
Reset();
|
||||
}
|
||||
|
||||
void AlbumCoverManager::showEvent(QShowEvent *) {
|
||||
Reset();
|
||||
}
|
||||
@ -185,7 +180,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem* current) {
|
||||
|
||||
// Get the list of albums. How we do it depends on what thing we have
|
||||
// selected in the artist list.
|
||||
LibraryBackendInterface::AlbumList albums;
|
||||
LibraryBackend::AlbumList albums;
|
||||
switch (current->type()) {
|
||||
case Various_Artists: albums = backend_->GetCompilationAlbums(); break;
|
||||
case Specific_Artist: albums = backend_->GetAlbumsByArtist(current->text()); break;
|
||||
@ -193,7 +188,7 @@ void AlbumCoverManager::ArtistChanged(QListWidgetItem* current) {
|
||||
default: albums = backend_->GetAllAlbums(); break;
|
||||
}
|
||||
|
||||
foreach (const LibraryBackendInterface::Album& info, albums) {
|
||||
foreach (const LibraryBackend::Album& info, albums) {
|
||||
// Don't show songs without an album, obviously
|
||||
if (info.album_name.isEmpty())
|
||||
continue;
|
||||
|
@ -20,15 +20,13 @@
|
||||
#include <QDialog>
|
||||
#include <QIcon>
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "gtest/gtest_prod.h"
|
||||
|
||||
#include "ui_albumcovermanager.h"
|
||||
#include "backgroundthread.h"
|
||||
#include "albumcoverloader.h"
|
||||
|
||||
class LibraryBackendInterface;
|
||||
class LibraryBackend;
|
||||
class AlbumCoverFetcher;
|
||||
|
||||
class QNetworkAccessManager;
|
||||
@ -36,7 +34,8 @@ class QNetworkAccessManager;
|
||||
class AlbumCoverManager : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AlbumCoverManager(QNetworkAccessManager* network, QWidget *parent = 0);
|
||||
AlbumCoverManager(QNetworkAccessManager* network, LibraryBackend* backend,
|
||||
QWidget *parent = 0);
|
||||
~AlbumCoverManager();
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
@ -45,9 +44,6 @@ class AlbumCoverManager : public QDialog {
|
||||
|
||||
void Init();
|
||||
|
||||
public slots:
|
||||
void SetBackend(boost::shared_ptr<LibraryBackendInterface> backend);
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *);
|
||||
void closeEvent(QCloseEvent *);
|
||||
@ -98,7 +94,7 @@ class AlbumCoverManager : public QDialog {
|
||||
bool constructed_;
|
||||
|
||||
Ui::CoverManager ui_;
|
||||
boost::shared_ptr<LibraryBackendInterface> backend_;
|
||||
LibraryBackend* backend_;
|
||||
|
||||
QAction* filter_all_;
|
||||
QAction* filter_with_covers_;
|
||||
|
259
src/database.cpp
Normal file
259
src/database.cpp
Normal file
@ -0,0 +1,259 @@
|
||||
/* 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 "database.h"
|
||||
|
||||
#include <QLibrary>
|
||||
#include <QLibraryInfo>
|
||||
#include <QDir>
|
||||
#include <QCoreApplication>
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlQuery>
|
||||
#include <QVariant>
|
||||
|
||||
const char* Database::kDatabaseFilename = "clementine.db";
|
||||
const int Database::kSchemaVersion = 7;
|
||||
|
||||
int (*Database::_sqlite3_create_function) (
|
||||
sqlite3*, const char*, int, int, void*,
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*)) = NULL;
|
||||
int (*Database::_sqlite3_value_type) (sqlite3_value*) = NULL;
|
||||
sqlite_int64 (*Database::_sqlite3_value_int64) (sqlite3_value*) = NULL;
|
||||
const uchar* (*Database::_sqlite3_value_text) (sqlite3_value*) = NULL;
|
||||
void (*Database::_sqlite3_result_int64) (sqlite3_context*, sqlite_int64) = NULL;
|
||||
void* (*Database::_sqlite3_user_data) (sqlite3_context*) = NULL;
|
||||
|
||||
bool Database::sStaticInitDone = false;
|
||||
bool Database::sLoadedSqliteSymbols = false;
|
||||
|
||||
|
||||
void Database::StaticInit() {
|
||||
if (sStaticInitDone) {
|
||||
return;
|
||||
}
|
||||
sStaticInitDone = true;
|
||||
|
||||
#ifndef Q_WS_X11
|
||||
// We statically link libqsqlite.dll on windows and mac so these symbols are already
|
||||
// available
|
||||
_sqlite3_create_function = sqlite3_create_function;
|
||||
_sqlite3_value_type = sqlite3_value_type;
|
||||
_sqlite3_value_int64 = sqlite3_value_int64;
|
||||
_sqlite3_value_text = sqlite3_value_text;
|
||||
_sqlite3_result_int64 = sqlite3_result_int64;
|
||||
_sqlite3_user_data = sqlite3_user_data;
|
||||
sLoadedSqliteSymbols = true;
|
||||
return;
|
||||
#else // Q_WS_X11
|
||||
QString plugin_path = QLibraryInfo::location(QLibraryInfo::PluginsPath) +
|
||||
"/sqldrivers/libqsqlite";
|
||||
|
||||
QLibrary library(plugin_path);
|
||||
if (!library.load()) {
|
||||
qDebug() << "QLibrary::load() failed for " << plugin_path;
|
||||
return;
|
||||
}
|
||||
|
||||
_sqlite3_create_function = reinterpret_cast<Sqlite3CreateFunc>(
|
||||
library.resolve("sqlite3_create_function"));
|
||||
_sqlite3_value_type = reinterpret_cast<int (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_type"));
|
||||
_sqlite3_value_int64 = reinterpret_cast<sqlite_int64 (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_int64"));
|
||||
_sqlite3_value_text = reinterpret_cast<const uchar* (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_text"));
|
||||
_sqlite3_result_int64 = reinterpret_cast<void (*) (sqlite3_context*, sqlite_int64)>(
|
||||
library.resolve("sqlite3_result_int64"));
|
||||
_sqlite3_user_data = reinterpret_cast<void* (*) (sqlite3_context*)>(
|
||||
library.resolve("sqlite3_user_data"));
|
||||
|
||||
if (!_sqlite3_create_function ||
|
||||
!_sqlite3_value_type ||
|
||||
!_sqlite3_value_int64 ||
|
||||
!_sqlite3_value_text ||
|
||||
!_sqlite3_result_int64 ||
|
||||
!_sqlite3_user_data) {
|
||||
qDebug() << "Couldn't resolve sqlite symbols";
|
||||
sLoadedSqliteSymbols = false;
|
||||
} else {
|
||||
sLoadedSqliteSymbols = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Database::Like(const char* needle, const char* haystack) {
|
||||
uint hash = qHash(needle);
|
||||
if (!query_hash_ || hash != query_hash_) {
|
||||
// New query, parse and cache.
|
||||
query_cache_ = QString::fromUtf8(needle).section('%', 1, 1).split(' ');
|
||||
query_hash_ = hash;
|
||||
}
|
||||
QString b = QString::fromUtf8(haystack);
|
||||
foreach (const QString& query, query_cache_) {
|
||||
if (!b.contains(query, Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Custom LIKE(X, Y) function for sqlite3 that supports case insensitive unicode matching.
|
||||
void Database::SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv) {
|
||||
Q_ASSERT(argc == 2 || argc == 3);
|
||||
Q_ASSERT(_sqlite3_value_type(argv[0]) == _sqlite3_value_type(argv[1]));
|
||||
|
||||
Database* library = reinterpret_cast<Database*>(_sqlite3_user_data(context));
|
||||
Q_ASSERT(library);
|
||||
|
||||
switch (_sqlite3_value_type(argv[0])) {
|
||||
case SQLITE_INTEGER: {
|
||||
qint64 result = _sqlite3_value_int64(argv[0]) - _sqlite3_value_int64(argv[1]);
|
||||
_sqlite3_result_int64(context, result ? 0 : 1);
|
||||
break;
|
||||
}
|
||||
case SQLITE_TEXT: {
|
||||
const char* data_a = reinterpret_cast<const char*>(_sqlite3_value_text(argv[0]));
|
||||
const char* data_b = reinterpret_cast<const char*>(_sqlite3_value_text(argv[1]));
|
||||
_sqlite3_result_int64(context, library->Like(data_a, data_b) ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Database::Database(QObject* parent, const QString& database_name)
|
||||
: QObject(parent),
|
||||
injected_database_name_(database_name),
|
||||
query_hash_(0)
|
||||
{
|
||||
directory_ = QDir::toNativeSeparators(
|
||||
QDir::homePath() + "/.config/" + QCoreApplication::organizationName());
|
||||
|
||||
Connect();
|
||||
}
|
||||
|
||||
QSqlDatabase Database::Connect() {
|
||||
QMutexLocker l(&connect_mutex_);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!QFile::exists(directory_)) {
|
||||
QDir dir;
|
||||
if (!dir.mkpath(directory_)) {
|
||||
}
|
||||
}
|
||||
|
||||
const QString connection_id("thread_" + QString::number(
|
||||
reinterpret_cast<quint64>(QThread::currentThread())));
|
||||
|
||||
// Try to find an existing connection for this thread
|
||||
QSqlDatabase db = QSqlDatabase::database(connection_id);
|
||||
if (db.isOpen()) {
|
||||
return db;
|
||||
}
|
||||
|
||||
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
|
||||
|
||||
if (!injected_database_name_.isNull())
|
||||
db.setDatabaseName(injected_database_name_);
|
||||
else
|
||||
db.setDatabaseName(directory_ + "/" + kDatabaseFilename);
|
||||
|
||||
if (!db.open()) {
|
||||
emit Error("Database: " + db.lastError().text());
|
||||
return db;
|
||||
}
|
||||
|
||||
// Find Sqlite3 functions in the Qt plugin.
|
||||
StaticInit();
|
||||
|
||||
// We want Unicode aware LIKE clauses if possible
|
||||
if (sLoadedSqliteSymbols) {
|
||||
QVariant v = db.driver()->handle();
|
||||
if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
|
||||
sqlite3* handle = *static_cast<sqlite3**>(v.data());
|
||||
if (handle) {
|
||||
_sqlite3_create_function(
|
||||
handle, // Sqlite3 handle.
|
||||
"LIKE", // Function name (either override or new).
|
||||
2, // Number of args.
|
||||
SQLITE_ANY, // What types this function accepts.
|
||||
this, // Custom data available via sqlite3_user_data().
|
||||
&Database::SqliteLike, // Our function :-)
|
||||
NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (db.tables().count() == 0) {
|
||||
// Set up initial schema
|
||||
UpdateDatabaseSchema(0, db);
|
||||
}
|
||||
|
||||
// Get the database's schema version
|
||||
QSqlQuery q("SELECT version FROM schema_version", db);
|
||||
int schema_version = 0;
|
||||
if (q.next())
|
||||
schema_version = q.value(0).toInt();
|
||||
|
||||
if (schema_version > kSchemaVersion) {
|
||||
qWarning() << "The database schema (version" << schema_version << ") is newer than I was expecting";
|
||||
return db;
|
||||
}
|
||||
if (schema_version < kSchemaVersion) {
|
||||
// Update the schema
|
||||
for (int v=schema_version+1 ; v<= kSchemaVersion ; ++v) {
|
||||
UpdateDatabaseSchema(v, db);
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
void Database::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
||||
QString filename;
|
||||
if (version == 0)
|
||||
filename = ":/schema.sql";
|
||||
else
|
||||
filename = QString(":/schema-%1.sql").arg(version);
|
||||
|
||||
// Open and read the database schema
|
||||
QFile schema_file(filename);
|
||||
if (!schema_file.open(QIODevice::ReadOnly))
|
||||
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
|
||||
QString schema(QString::fromUtf8(schema_file.readAll()));
|
||||
|
||||
// Run each command
|
||||
QStringList commands(schema.split(";\n\n"));
|
||||
db.transaction();
|
||||
foreach (const QString& command, commands) {
|
||||
QSqlQuery query(db.exec(command));
|
||||
if (CheckErrors(query.lastError()))
|
||||
qFatal("Unable to update music library database");
|
||||
}
|
||||
db.commit();
|
||||
}
|
||||
|
||||
bool Database::CheckErrors(const QSqlError& error) {
|
||||
if (error.isValid()) {
|
||||
qDebug() << error;
|
||||
emit Error("LibraryBackend: " + error.text());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
98
src/database.h
Normal file
98
src/database.h
Normal file
@ -0,0 +1,98 @@
|
||||
/* 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 DATABASE_H
|
||||
#define DATABASE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlError>
|
||||
#include <QMutex>
|
||||
#include <QStringList>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "gtest/gtest_prod.h"
|
||||
|
||||
class Database : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Database(QObject* parent = 0, const QString& database_name = QString());
|
||||
|
||||
static const int kSchemaVersion;
|
||||
static const char* kDatabaseFilename;
|
||||
|
||||
QSqlDatabase Connect();
|
||||
bool CheckErrors(const QSqlError& error);
|
||||
|
||||
signals:
|
||||
void Error(const QString& message);
|
||||
|
||||
private:
|
||||
void UpdateDatabaseSchema(int version, QSqlDatabase& db);
|
||||
|
||||
QString directory_;
|
||||
QMutex connect_mutex_;
|
||||
|
||||
// Used by tests
|
||||
QString injected_database_name_;
|
||||
|
||||
uint query_hash_;
|
||||
QStringList query_cache_;
|
||||
|
||||
FRIEND_TEST(DatabaseTest, LikeWorksWithAllAscii);
|
||||
FRIEND_TEST(DatabaseTest, LikeWorksWithUnicode);
|
||||
FRIEND_TEST(DatabaseTest, LikeAsciiCaseInsensitive);
|
||||
FRIEND_TEST(DatabaseTest, LikeUnicodeCaseInsensitive);
|
||||
FRIEND_TEST(DatabaseTest, LikePerformance);
|
||||
FRIEND_TEST(DatabaseTest, LikeCacheInvalidated);
|
||||
FRIEND_TEST(DatabaseTest, LikeQuerySplit);
|
||||
|
||||
// Do static initialisation like loading sqlite functions.
|
||||
static void StaticInit();
|
||||
|
||||
// Custom LIKE() function for sqlite.
|
||||
bool Like(const char* needle, const char* haystack);
|
||||
static void SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv);
|
||||
typedef int (*Sqlite3CreateFunc) (
|
||||
sqlite3*, const char*, int, int, void*,
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*));
|
||||
|
||||
// Sqlite3 functions. These will be loaded from the sqlite3 plugin.
|
||||
static Sqlite3CreateFunc _sqlite3_create_function;
|
||||
static int (*_sqlite3_value_type) (sqlite3_value*);
|
||||
static sqlite_int64 (*_sqlite3_value_int64) (sqlite3_value*);
|
||||
static const uchar* (*_sqlite3_value_text) (sqlite3_value*);
|
||||
static void (*_sqlite3_result_int64) (sqlite3_context*, sqlite_int64);
|
||||
static void* (*_sqlite3_user_data) (sqlite3_context*);
|
||||
|
||||
static bool sStaticInitDone;
|
||||
static bool sLoadedSqliteSymbols;
|
||||
};
|
||||
|
||||
class MemoryDatabase : public Database {
|
||||
public:
|
||||
MemoryDatabase(QObject* parent = 0) : Database(parent, ":memory:") {}
|
||||
~MemoryDatabase() {
|
||||
// Make sure Qt doesn't reuse the same database
|
||||
QSqlDatabase::removeDatabase(Connect().connectionName());
|
||||
}
|
||||
};
|
||||
|
||||
#endif // DATABASE_H
|
@ -104,9 +104,9 @@ bool EditTagDialog::SetSongs(const SongList &s) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditTagDialog::SetTagCompleter(Library* library) {
|
||||
new TagCompleter(library, Playlist::Column_Artist, ui_.artist);
|
||||
new TagCompleter(library, Playlist::Column_Album, ui_.album);
|
||||
void EditTagDialog::SetTagCompleter(LibraryBackend* backend) {
|
||||
new TagCompleter(backend, Playlist::Column_Artist, ui_.artist);
|
||||
new TagCompleter(backend, Playlist::Column_Album, ui_.album);
|
||||
}
|
||||
|
||||
void EditTagDialog::accept() {
|
||||
|
@ -22,7 +22,7 @@
|
||||
#include "ui_edittagdialog.h"
|
||||
#include "song.h"
|
||||
|
||||
class Library;
|
||||
class LibraryBackend;
|
||||
|
||||
class EditTagDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
@ -33,7 +33,7 @@ class EditTagDialog : public QDialog {
|
||||
static const char* kHintText;
|
||||
|
||||
bool SetSongs(const SongList& songs);
|
||||
void SetTagCompleter(Library* library);
|
||||
void SetTagCompleter(LibraryBackend* backend);
|
||||
|
||||
public slots:
|
||||
void accept();
|
||||
|
@ -24,14 +24,14 @@ GroupByDialog::GroupByDialog(QWidget *parent)
|
||||
ui_.setupUi(this);
|
||||
Reset();
|
||||
|
||||
mapping_.insert(Mapping(Library::GroupBy_None, 0));
|
||||
mapping_.insert(Mapping(Library::GroupBy_Album, 1));
|
||||
mapping_.insert(Mapping(Library::GroupBy_Artist, 2));
|
||||
mapping_.insert(Mapping(Library::GroupBy_AlbumArtist, 3));
|
||||
mapping_.insert(Mapping(Library::GroupBy_Composer, 4));
|
||||
mapping_.insert(Mapping(Library::GroupBy_Genre, 5));
|
||||
mapping_.insert(Mapping(Library::GroupBy_Year, 6));
|
||||
mapping_.insert(Mapping(Library::GroupBy_YearAlbum, 7));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_None, 0));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_Album, 1));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_Artist, 2));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_AlbumArtist, 3));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_Composer, 4));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_Genre, 5));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_Year, 6));
|
||||
mapping_.insert(Mapping(LibraryModel::GroupBy_YearAlbum, 7));
|
||||
|
||||
connect(ui_.button_box->button(QDialogButtonBox::Reset), SIGNAL(clicked()),
|
||||
SLOT(Reset()));
|
||||
@ -44,14 +44,14 @@ void GroupByDialog::Reset() {
|
||||
}
|
||||
|
||||
void GroupByDialog::accept() {
|
||||
emit Accepted(Library::Grouping(
|
||||
emit Accepted(LibraryModel::Grouping(
|
||||
mapping_.get<tag_index>().find(ui_.first->currentIndex())->group_by,
|
||||
mapping_.get<tag_index>().find(ui_.second->currentIndex())->group_by,
|
||||
mapping_.get<tag_index>().find(ui_.third->currentIndex())->group_by));
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void GroupByDialog::LibraryGroupingChanged(const Library::Grouping& g) {
|
||||
void GroupByDialog::LibraryGroupingChanged(const LibraryModel::Grouping& g) {
|
||||
ui_.first->setCurrentIndex(mapping_.get<tag_group_by>().find(g[0])->combo_box_index);
|
||||
ui_.second->setCurrentIndex(mapping_.get<tag_group_by>().find(g[1])->combo_box_index);
|
||||
ui_.third->setCurrentIndex(mapping_.get<tag_group_by>().find(g[2])->combo_box_index);
|
||||
|
@ -23,7 +23,7 @@
|
||||
#include <boost/multi_index/member.hpp>
|
||||
#include <boost/multi_index/ordered_index.hpp>
|
||||
|
||||
#include "library.h"
|
||||
#include "librarymodel.h"
|
||||
#include "ui_groupbydialog.h"
|
||||
|
||||
using boost::multi_index_container;
|
||||
@ -39,20 +39,20 @@ class GroupByDialog : public QDialog {
|
||||
GroupByDialog(QWidget *parent = 0);
|
||||
|
||||
public slots:
|
||||
void LibraryGroupingChanged(const Library::Grouping& g);
|
||||
void LibraryGroupingChanged(const LibraryModel::Grouping& g);
|
||||
void accept();
|
||||
|
||||
signals:
|
||||
void Accepted(const Library::Grouping& g);
|
||||
void Accepted(const LibraryModel::Grouping& g);
|
||||
|
||||
private slots:
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
struct Mapping {
|
||||
Mapping(Library::GroupBy g, int i) : group_by(g), combo_box_index(i) {}
|
||||
Mapping(LibraryModel::GroupBy g, int i) : group_by(g), combo_box_index(i) {}
|
||||
|
||||
Library::GroupBy group_by;
|
||||
LibraryModel::GroupBy group_by;
|
||||
int combo_box_index;
|
||||
};
|
||||
|
||||
@ -64,7 +64,7 @@ class GroupByDialog : public QDialog {
|
||||
ordered_unique<tag<tag_index>,
|
||||
member<Mapping, int, &Mapping::combo_box_index> >,
|
||||
ordered_unique<tag<tag_group_by>,
|
||||
member<Mapping, Library::GroupBy, &Mapping::group_by> >
|
||||
member<Mapping, LibraryModel::GroupBy, &Mapping::group_by> >
|
||||
>
|
||||
> MappingContainer;
|
||||
|
||||
|
837
src/library.cpp
837
src/library.cpp
@ -15,44 +15,19 @@
|
||||
*/
|
||||
|
||||
#include "library.h"
|
||||
#include "librarymodel.h"
|
||||
#include "librarybackend.h"
|
||||
#include "libraryitem.h"
|
||||
#include "songmimedata.h"
|
||||
#include "librarydirectorymodel.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QMetaEnum>
|
||||
const char* Library::kSongsTable = "songs";
|
||||
const char* Library::kDirsTable = "directories";
|
||||
const char* Library::kSubdirsTable = "subdirectories";
|
||||
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
|
||||
Library::Library(QObject* parent, const QString& table)
|
||||
: SimpleTreeModel<LibraryItem>(new LibraryItem(this), parent),
|
||||
backend_factory_(new BackgroundThreadFactoryImplementation<LibraryBackendInterface, LibraryBackend>),
|
||||
Library::Library(Database *db, QObject *parent)
|
||||
: backend_(new LibraryBackend(db, kSongsTable, kDirsTable, kSubdirsTable, this)),
|
||||
model_(new LibraryModel(backend_, parent)),
|
||||
watcher_factory_(new BackgroundThreadFactoryImplementation<LibraryWatcher, LibraryWatcher>),
|
||||
backend_(NULL),
|
||||
watcher_(NULL),
|
||||
dir_model_(new LibraryDirectoryModel(this)),
|
||||
waiting_for_threads_(2),
|
||||
query_options_(table),
|
||||
artist_icon_(":artist.png"),
|
||||
album_icon_(":album.png"),
|
||||
no_cover_icon_(":nocover.png")
|
||||
watcher_(NULL)
|
||||
{
|
||||
root_->lazy_loaded = true;
|
||||
|
||||
group_by_[0] = GroupBy_Artist;
|
||||
group_by_[1] = GroupBy_Album;
|
||||
group_by_[2] = GroupBy_None;
|
||||
}
|
||||
|
||||
Library::~Library() {
|
||||
delete root_;
|
||||
}
|
||||
|
||||
void Library::set_backend_factory(BackgroundThreadFactory<LibraryBackendInterface>* factory) {
|
||||
backend_factory_.reset(factory);
|
||||
}
|
||||
|
||||
void Library::set_watcher_factory(BackgroundThreadFactory<LibraryWatcher>* factory) {
|
||||
@ -60,799 +35,43 @@ void Library::set_watcher_factory(BackgroundThreadFactory<LibraryWatcher>* facto
|
||||
}
|
||||
|
||||
void Library::Init() {
|
||||
backend_ = backend_factory_->GetThread(this);
|
||||
watcher_ = watcher_factory_->GetThread(this);
|
||||
|
||||
connect(backend_, SIGNAL(Initialised()), SLOT(BackendInitialised()));
|
||||
connect(watcher_, SIGNAL(Initialised()), SLOT(WatcherInitialised()));
|
||||
}
|
||||
|
||||
void Library::StartThreads() {
|
||||
Q_ASSERT(waiting_for_threads_);
|
||||
Q_ASSERT(backend_);
|
||||
Q_ASSERT(watcher_);
|
||||
|
||||
backend_->Start();
|
||||
|
||||
watcher_->set_io_priority(BackgroundThreadBase::IOPRIO_CLASS_IDLE);
|
||||
watcher_->set_cpu_priority(QThread::IdlePriority);
|
||||
watcher_->Start();
|
||||
}
|
||||
|
||||
void Library::BackendInitialised() {
|
||||
connect(backend_->Worker().get(), SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
|
||||
connect(backend_->Worker().get(), SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList)));
|
||||
connect(backend_->Worker().get(), SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(backend_->Worker().get(), SIGNAL(TotalSongCountUpdated(int)), SIGNAL(TotalSongCountUpdated(int)));
|
||||
|
||||
dir_model_->SetBackend(backend_->Worker());
|
||||
|
||||
emit BackendReady(backend_->Worker());
|
||||
|
||||
if (--waiting_for_threads_ == 0)
|
||||
Initialise();
|
||||
model_->Init();
|
||||
}
|
||||
|
||||
void Library::WatcherInitialised() {
|
||||
connect(watcher_->Worker().get(), SIGNAL(ScanStarted()), SIGNAL(ScanStarted()));
|
||||
connect(watcher_->Worker().get(), SIGNAL(ScanFinished()), SIGNAL(ScanFinished()));
|
||||
LibraryWatcher* watcher = watcher_->Worker().get();
|
||||
connect(watcher, SIGNAL(ScanStarted()), SIGNAL(ScanStarted()));
|
||||
connect(watcher, SIGNAL(ScanFinished()), SIGNAL(ScanFinished()));
|
||||
|
||||
if (--waiting_for_threads_ == 0)
|
||||
Initialise();
|
||||
}
|
||||
watcher->SetBackend(backend_);
|
||||
|
||||
void Library::Initialise() {
|
||||
// The backend and watcher threads are finished initialising, now we can
|
||||
// connect them together and start everything off.
|
||||
watcher_->Worker()->SetBackend(backend_->Worker());
|
||||
|
||||
connect(backend_->Worker().get(), SIGNAL(DirectoryDiscovered(Directory,SubdirectoryList)),
|
||||
watcher_->Worker().get(), SLOT(AddDirectory(Directory,SubdirectoryList)));
|
||||
connect(backend_->Worker().get(), SIGNAL(DirectoryDeleted(Directory)),
|
||||
watcher_->Worker().get(), SLOT(RemoveDirectory(Directory)));
|
||||
connect(watcher_->Worker().get(), SIGNAL(NewOrUpdatedSongs(SongList)),
|
||||
backend_->Worker().get(), SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(watcher_->Worker().get(), SIGNAL(SongsMTimeUpdated(SongList)),
|
||||
backend_->Worker().get(), SLOT(UpdateMTimesOnly(SongList)));
|
||||
connect(watcher_->Worker().get(), SIGNAL(SongsDeleted(SongList)),
|
||||
backend_->Worker().get(), SLOT(DeleteSongs(SongList)));
|
||||
connect(watcher_->Worker().get(), SIGNAL(SubdirsDiscovered(SubdirectoryList)),
|
||||
backend_->Worker().get(), SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_->Worker().get(), SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)),
|
||||
backend_->Worker().get(), SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory,SubdirectoryList)),
|
||||
watcher, SLOT(AddDirectory(Directory,SubdirectoryList)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)),
|
||||
watcher, SLOT(RemoveDirectory(Directory)));
|
||||
connect(watcher, SIGNAL(NewOrUpdatedSongs(SongList)),
|
||||
backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(watcher, SIGNAL(SongsMTimeUpdated(SongList)),
|
||||
backend_, SLOT(UpdateMTimesOnly(SongList)));
|
||||
connect(watcher, SIGNAL(SongsDeleted(SongList)),
|
||||
backend_, SLOT(DeleteSongs(SongList)));
|
||||
connect(watcher, SIGNAL(SubdirsDiscovered(SubdirectoryList)),
|
||||
backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher, SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)),
|
||||
backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
|
||||
// This will start the watcher checking for updates
|
||||
backend_->Worker()->LoadDirectoriesAsync();
|
||||
|
||||
backend_->Worker()->UpdateTotalSongCountAsync();
|
||||
|
||||
Reset();
|
||||
backend_->LoadDirectoriesAsync();
|
||||
}
|
||||
|
||||
void Library::SongsDiscovered(const SongList& songs) {
|
||||
foreach (const Song& song, songs) {
|
||||
// Sanity check to make sure we don't add songs that are outside the user's
|
||||
// filter
|
||||
if (!query_options_.Matches(song))
|
||||
continue;
|
||||
|
||||
// Hey, we've already got that one!
|
||||
if (song_nodes_.contains(song.id()))
|
||||
continue;
|
||||
|
||||
// Before we can add each song we need to make sure the required container
|
||||
// items already exist in the tree. These depend on which "group by"
|
||||
// settings the user has on the library. Eg. if the user grouped by
|
||||
// artist and album, we would need to make sure nodes for the song's artist
|
||||
// and album were already in the tree.
|
||||
|
||||
// Find parent containers in the tree
|
||||
LibraryItem* container = root_;
|
||||
for (int i=0 ; i<3 ; ++i) {
|
||||
GroupBy type = group_by_[i];
|
||||
if (type == GroupBy_None) break;
|
||||
|
||||
// Special case: if we're at the top level and the song is a compilation
|
||||
// and the top level is Artists, then we want the Various Artists node :(
|
||||
if (i == 0 && type == GroupBy_Artist && song.is_compilation()) {
|
||||
if (compilation_artist_node_ == NULL)
|
||||
CreateCompilationArtistNode(true, root_);
|
||||
container = compilation_artist_node_;
|
||||
} else {
|
||||
// Otherwise find the proper container at this level based on the
|
||||
// item's key
|
||||
QString key;
|
||||
switch (type) {
|
||||
case GroupBy_Album: key = song.album(); break;
|
||||
case GroupBy_Artist: key = song.artist(); break;
|
||||
case GroupBy_Composer: key = song.composer(); break;
|
||||
case GroupBy_Genre: key = song.genre(); break;
|
||||
case GroupBy_AlbumArtist: key = song.albumartist(); break;
|
||||
case GroupBy_Year:
|
||||
key = QString::number(qMax(0, song.year())); break;
|
||||
case GroupBy_YearAlbum:
|
||||
key = PrettyYearAlbum(qMax(0, song.year()), song.album()); break;
|
||||
case GroupBy_None: Q_ASSERT(0); break;
|
||||
}
|
||||
|
||||
// Does it exist already?
|
||||
if (!container_nodes_[i].contains(key)) {
|
||||
// Create the container
|
||||
container_nodes_[i][key] =
|
||||
ItemFromSong(type, true, i == 0, container, song, i);
|
||||
}
|
||||
container = container_nodes_[i][key];
|
||||
}
|
||||
|
||||
// If we just created the damn thing then we don't need to continue into
|
||||
// it any further because it'll get lazy-loaded properly later.
|
||||
if (!container->lazy_loaded)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!container->lazy_loaded)
|
||||
continue;
|
||||
|
||||
// We've gone all the way down to the deepest level and everything was
|
||||
// already lazy loaded, so now we have to create the song in the container.
|
||||
song_nodes_[song.id()] =
|
||||
ItemFromSong(GroupBy_None, true, false, container, song, -1);
|
||||
}
|
||||
}
|
||||
|
||||
LibraryItem* Library::CreateCompilationArtistNode(bool signal, LibraryItem* parent) {
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
|
||||
|
||||
compilation_artist_node_ =
|
||||
new LibraryItem(LibraryItem::Type_Container, parent);
|
||||
compilation_artist_node_->key = tr("Various Artists");
|
||||
compilation_artist_node_->sort_text = " various";
|
||||
compilation_artist_node_->container_level = parent->container_level + 1;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
|
||||
return compilation_artist_node_;
|
||||
}
|
||||
|
||||
QString Library::DividerKey(GroupBy type, LibraryItem* item) const {
|
||||
// Items which are to be grouped under the same divider must produce the
|
||||
// same divider key. This will only get called for top-level items.
|
||||
|
||||
if (item->sort_text.isEmpty())
|
||||
return QString();
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_Artist:
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_AlbumArtist:
|
||||
if (item->sort_text[0].isDigit())
|
||||
return "0";
|
||||
if (item->sort_text[0] == ' ')
|
||||
return QString();
|
||||
return QString(item->sort_text[0]);
|
||||
|
||||
case GroupBy_Year:
|
||||
return SortTextForYear(item->sort_text.toInt() / 10 * 10);
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
return SortTextForYear(item->metadata.year());
|
||||
|
||||
case GroupBy_None:
|
||||
default:
|
||||
Q_ASSERT(0);
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString Library::DividerDisplayText(GroupBy type, const QString& key) const {
|
||||
// Pretty display text for the dividers.
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_Artist:
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_AlbumArtist:
|
||||
if (key == "0")
|
||||
return "0-9";
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
if (key == "0000")
|
||||
return tr("Unknown");
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy_Year:
|
||||
if (key == "0000")
|
||||
return tr("Unknown");
|
||||
return QString::number(key.toInt()); // To remove leading 0s
|
||||
|
||||
case GroupBy_None:
|
||||
default:
|
||||
Q_ASSERT(0);
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
void Library::SongsDeleted(const SongList& songs) {
|
||||
// Delete the actual song nodes first, keeping track of each parent so we
|
||||
// might check to see if they're empty later.
|
||||
QSet<LibraryItem*> parents;
|
||||
foreach (const Song& song, songs) {
|
||||
if (song_nodes_.contains(song.id())) {
|
||||
LibraryItem* node = song_nodes_[song.id()];
|
||||
|
||||
if (node->parent != root_)
|
||||
parents << node->parent;
|
||||
|
||||
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
||||
node->parent->Delete(node->row);
|
||||
song_nodes_.remove(song.id());
|
||||
endRemoveRows();
|
||||
} else {
|
||||
// If we get here it means some of the songs we want to delete haven't
|
||||
// been lazy-loaded yet. This is bad, because it would mean that to
|
||||
// clean up empty parents we would need to lazy-load them all
|
||||
// individually to see if they're empty. This can take a very long time,
|
||||
// so better to just reset the model and be done with it.
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Now delete empty parents
|
||||
QSet<QString> divider_keys;
|
||||
while (!parents.isEmpty()) {
|
||||
foreach (LibraryItem* node, parents) {
|
||||
parents.remove(node);
|
||||
if (node->children.count() != 0)
|
||||
continue;
|
||||
|
||||
// Consider its parent for the next round
|
||||
if (node->parent != root_)
|
||||
parents << node->parent;
|
||||
|
||||
// Maybe consider its divider node
|
||||
if (node->container_level == 0)
|
||||
divider_keys << DividerKey(group_by_[0], node);
|
||||
|
||||
// Special case the Various Artists node
|
||||
if (node == compilation_artist_node_)
|
||||
compilation_artist_node_ = NULL;
|
||||
else
|
||||
container_nodes_[node->container_level].remove(node->key);
|
||||
|
||||
// It was empty - delete it
|
||||
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
||||
node->parent->Delete(node->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete empty dividers
|
||||
foreach (const QString& divider_key, divider_keys) {
|
||||
if (!divider_nodes_.contains(divider_key))
|
||||
continue;
|
||||
|
||||
// Look to see if there are any other items still under this divider
|
||||
bool found = false;
|
||||
foreach (LibraryItem* node, container_nodes_[0].values()) {
|
||||
if (DividerKey(group_by_[0], node) == divider_key) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
continue;
|
||||
|
||||
// Remove the divider
|
||||
int row = divider_nodes_[divider_key]->row;
|
||||
beginRemoveRows(ItemToIndex(root_), row, row);
|
||||
root_->Delete(row);
|
||||
endRemoveRows();
|
||||
divider_nodes_.remove(divider_key);
|
||||
}
|
||||
}
|
||||
|
||||
QVariant Library::data(const QModelIndex& index, int role) const {
|
||||
const LibraryItem* item = IndexToItem(index);
|
||||
|
||||
return data(item, role);
|
||||
}
|
||||
|
||||
QVariant Library::data(const LibraryItem* item, int role) const {
|
||||
GroupBy container_type =
|
||||
item->type == LibraryItem::Type_Container ?
|
||||
group_by_[item->container_level] : GroupBy_None;
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
case Qt::ToolTipRole:
|
||||
return item->DisplayText();
|
||||
|
||||
case Qt::DecorationRole:
|
||||
switch (item->type)
|
||||
case LibraryItem::Type_Container:
|
||||
switch (container_type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_YearAlbum:
|
||||
return album_icon_;
|
||||
case GroupBy_Artist:
|
||||
return artist_icon_;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
break;
|
||||
|
||||
case Role_Type:
|
||||
return item->type;
|
||||
|
||||
case Role_ContainerType:
|
||||
return container_type;
|
||||
|
||||
case Role_Key:
|
||||
return item->key;
|
||||
|
||||
case Role_Artist:
|
||||
return item->metadata.artist();
|
||||
|
||||
case Role_SortText:
|
||||
if (item->type == LibraryItem::Type_Song)
|
||||
return item->metadata.disc() * 1000 + item->metadata.track();
|
||||
return item->SortText();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void Library::LazyPopulate(LibraryItem* parent, bool signal) {
|
||||
if (parent->lazy_loaded)
|
||||
return;
|
||||
parent->lazy_loaded = true;
|
||||
|
||||
// Information about what we want the children to be
|
||||
int child_level = parent->container_level + 1;
|
||||
GroupBy child_type = child_level >= 3 ? GroupBy_None : group_by_[child_level];
|
||||
|
||||
// Initialise the query. child_type says what type of thing we want (artists,
|
||||
// songs, etc.)
|
||||
LibraryQuery q(query_options_);
|
||||
InitQuery(child_type, &q);
|
||||
|
||||
// Top-level artists is special - we don't want compilation albums appearing
|
||||
if (child_level == 0 && child_type == GroupBy_Artist) {
|
||||
q.AddCompilationRequirement(false);
|
||||
}
|
||||
|
||||
// Walk up through the item's parents adding filters as necessary
|
||||
LibraryItem* p = parent;
|
||||
while (p != root_) {
|
||||
FilterQuery(group_by_[p->container_level], p, &q);
|
||||
p = p->parent;
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
if (!backend_->Worker()->ExecQuery(&q))
|
||||
return;
|
||||
|
||||
// Step through the results
|
||||
while (q.Next()) {
|
||||
// Create the item - it will get inserted into the model here
|
||||
LibraryItem* item =
|
||||
ItemFromQuery(child_type, signal, child_level == 0, parent, q, child_level);
|
||||
|
||||
// Save a pointer to it for later
|
||||
if (child_type == GroupBy_None)
|
||||
song_nodes_[item->metadata.id()] = item;
|
||||
else
|
||||
container_nodes_[child_level][item->key] = item;
|
||||
}
|
||||
}
|
||||
|
||||
void Library::Reset() {
|
||||
delete root_;
|
||||
song_nodes_.clear();
|
||||
container_nodes_[0].clear();
|
||||
container_nodes_[1].clear();
|
||||
container_nodes_[2].clear();
|
||||
divider_nodes_.clear();
|
||||
compilation_artist_node_ = NULL;
|
||||
|
||||
root_ = new LibraryItem(this);
|
||||
root_->lazy_loaded = false;
|
||||
|
||||
// Various artists?
|
||||
if (group_by_[0] == GroupBy_Artist &&
|
||||
backend_->Worker()->HasCompilations(query_options_))
|
||||
CreateCompilationArtistNode(false, root_);
|
||||
|
||||
// Populate top level
|
||||
LazyPopulate(root_, false);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void Library::InitQuery(GroupBy type, LibraryQuery* q) {
|
||||
// Say what type of thing we want to get back from the database.
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
q->SetColumnSpec("DISTINCT artist");
|
||||
break;
|
||||
case GroupBy_Album:
|
||||
q->SetColumnSpec("DISTINCT album");
|
||||
break;
|
||||
case GroupBy_Composer:
|
||||
q->SetColumnSpec("DISTINCT composer");
|
||||
break;
|
||||
case GroupBy_YearAlbum:
|
||||
q->SetColumnSpec("DISTINCT year, album");
|
||||
break;
|
||||
case GroupBy_Year:
|
||||
q->SetColumnSpec("DISTINCT year");
|
||||
break;
|
||||
case GroupBy_Genre:
|
||||
q->SetColumnSpec("DISTINCT genre");
|
||||
break;
|
||||
case GroupBy_AlbumArtist:
|
||||
q->SetColumnSpec("DISTINCT albumartist");
|
||||
break;
|
||||
case GroupBy_None:
|
||||
q->SetColumnSpec("ROWID, " + Song::kColumnSpec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Library::FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q) {
|
||||
// Say how we want the query to be filtered. This is done once for each
|
||||
// parent going up the tree.
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
if (item == compilation_artist_node_)
|
||||
q->AddCompilationRequirement(true);
|
||||
else {
|
||||
if (item->container_level == 0) // Stupid hack
|
||||
q->AddCompilationRequirement(false);
|
||||
q->AddWhere("artist", item->key);
|
||||
}
|
||||
break;
|
||||
case GroupBy_Album:
|
||||
q->AddWhere("album", item->key);
|
||||
break;
|
||||
case GroupBy_YearAlbum:
|
||||
q->AddWhere("year", item->metadata.year());
|
||||
q->AddWhere("album", item->metadata.album());
|
||||
break;
|
||||
case GroupBy_Year:
|
||||
q->AddWhere("year", item->key);
|
||||
break;
|
||||
case GroupBy_Composer:
|
||||
q->AddWhere("composer", item->key);
|
||||
break;
|
||||
case GroupBy_Genre:
|
||||
q->AddWhere("genre", item->key);
|
||||
break;
|
||||
case GroupBy_AlbumArtist:
|
||||
q->AddWhere("albumartist", item->key);
|
||||
break;
|
||||
case GroupBy_None:
|
||||
Q_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LibraryItem* Library::InitItem(GroupBy type, bool signal, LibraryItem *parent,
|
||||
int container_level) {
|
||||
LibraryItem::Type item_type =
|
||||
type == GroupBy_None ? LibraryItem::Type_Song :
|
||||
LibraryItem::Type_Container;
|
||||
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent),
|
||||
parent->children.count(),parent->children.count());
|
||||
|
||||
// Initialise the item depending on what type it's meant to be
|
||||
LibraryItem* item = new LibraryItem(item_type, parent);
|
||||
item->container_level = container_level;
|
||||
return item;
|
||||
}
|
||||
|
||||
LibraryItem* Library::ItemFromQuery(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem* parent, const LibraryQuery& q,
|
||||
int container_level) {
|
||||
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
||||
int year = 0;
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
item->key = q.Value(0).toString();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortTextForArtist(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
year = qMax(0, q.Value(0).toInt());
|
||||
item->metadata.set_year(year);
|
||||
item->metadata.set_album(q.Value(1).toString());
|
||||
item->key = PrettyYearAlbum(year, item->metadata.album());
|
||||
item->sort_text = SortTextForYear(year) + item->metadata.album();
|
||||
break;
|
||||
|
||||
case GroupBy_Year:
|
||||
year = qMax(0, q.Value(0).toInt());
|
||||
item->key = QString::number(year);
|
||||
item->sort_text = SortTextForYear(year) + " ";
|
||||
break;
|
||||
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_Album:
|
||||
case GroupBy_AlbumArtist:
|
||||
item->key = q.Value(0).toString();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortText(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_None:
|
||||
item->metadata.InitFromQuery(q);
|
||||
item->key = item->metadata.title();
|
||||
item->display_text = item->metadata.PrettyTitleWithArtist();
|
||||
break;
|
||||
}
|
||||
|
||||
FinishItem(type, signal, create_divider, parent, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
LibraryItem* Library::ItemFromSong(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem* parent, const Song& s,
|
||||
int container_level) {
|
||||
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
||||
int year = 0;
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
item->key = s.artist();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortTextForArtist(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
year = qMax(0, s.year());
|
||||
item->metadata.set_year(year);
|
||||
item->metadata.set_album(s.album());
|
||||
item->key = PrettyYearAlbum(year, s.album());
|
||||
item->sort_text = SortTextForYear(year) + s.album();
|
||||
break;
|
||||
|
||||
case GroupBy_Year:
|
||||
year = qMax(0, s.year());
|
||||
item->key = QString::number(year);
|
||||
item->sort_text = SortTextForYear(year) + " ";
|
||||
break;
|
||||
|
||||
case GroupBy_Composer: item->key = s.composer();
|
||||
case GroupBy_Genre: if (item->key.isNull()) item->key = s.genre();
|
||||
case GroupBy_Album: if (item->key.isNull()) item->key = s.album();
|
||||
case GroupBy_AlbumArtist: if (item->key.isNull()) item->key = s.albumartist();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortText(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_None:
|
||||
item->metadata = s;
|
||||
item->key = s.title();
|
||||
item->display_text = s.PrettyTitleWithArtist();
|
||||
break;
|
||||
}
|
||||
|
||||
FinishItem(type, signal, create_divider, parent, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
void Library::FinishItem(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem *parent, LibraryItem *item) {
|
||||
if (type == GroupBy_None)
|
||||
item->lazy_loaded = true;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
|
||||
// Create the divider entry if we're supposed to
|
||||
if (create_divider) {
|
||||
QString divider_key = DividerKey(type, item);
|
||||
|
||||
if (!divider_key.isEmpty() && !divider_nodes_.contains(divider_key)) {
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent), parent->children.count(),
|
||||
parent->children.count());
|
||||
|
||||
LibraryItem* divider =
|
||||
new LibraryItem(LibraryItem::Type_Divider, root_);
|
||||
divider->key = divider_key;
|
||||
divider->display_text = DividerDisplayText(type, divider_key);
|
||||
divider->lazy_loaded = true;
|
||||
|
||||
divider_nodes_[divider_key] = divider;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Library::TextOrUnknown(const QString& text) const {
|
||||
if (text.isEmpty()) {
|
||||
return tr("Unknown");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
QString Library::PrettyYearAlbum(int year, const QString& album) const {
|
||||
if (year <= 0)
|
||||
return TextOrUnknown(album);
|
||||
return QString::number(year) + " - " + TextOrUnknown(album);
|
||||
}
|
||||
|
||||
QString Library::SortText(QString text) const {
|
||||
if (text.isEmpty()) {
|
||||
text = " unknown";
|
||||
} else {
|
||||
text = text.toLower();
|
||||
}
|
||||
text = text.remove(QRegExp("[^\\w ]"));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
QString Library::SortTextForArtist(QString artist) const {
|
||||
artist = SortText(artist);
|
||||
|
||||
if (artist.startsWith("the ")) {
|
||||
artist = artist.right(artist.length() - 4) + ", the";
|
||||
}
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
QString Library::SortTextForYear(int year) const {
|
||||
QString str = QString::number(year);
|
||||
return QString("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||
}
|
||||
|
||||
Qt::ItemFlags Library::flags(const QModelIndex& index) const {
|
||||
switch (IndexToItem(index)->type) {
|
||||
case LibraryItem::Type_Song:
|
||||
case LibraryItem::Type_Container:
|
||||
return Qt::ItemIsSelectable |
|
||||
Qt::ItemIsEnabled |
|
||||
Qt::ItemIsDragEnabled;
|
||||
case LibraryItem::Type_Divider:
|
||||
case LibraryItem::Type_Root:
|
||||
default:
|
||||
return Qt::ItemIsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
QStringList Library::mimeTypes() const {
|
||||
return QStringList() << "text/uri-list";
|
||||
}
|
||||
|
||||
QMimeData* Library::mimeData(const QModelIndexList& indexes) const {
|
||||
SongMimeData* data = new SongMimeData;
|
||||
QList<QUrl> urls;
|
||||
QSet<int> song_ids;
|
||||
|
||||
foreach (const QModelIndex& index, indexes) {
|
||||
GetChildSongs(IndexToItem(index), &urls, &data->songs, &song_ids);
|
||||
}
|
||||
|
||||
data->setUrls(urls);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool Library::CompareItems(const LibraryItem* a, const LibraryItem* b) const {
|
||||
QVariant left(data(a, Library::Role_SortText));
|
||||
QVariant right(data(b, Library::Role_SortText));
|
||||
|
||||
if (left.type() == QVariant::Int)
|
||||
return left.toInt() < right.toInt();
|
||||
return left.toString() < right.toString();
|
||||
}
|
||||
|
||||
void Library::GetChildSongs(LibraryItem* item, QList<QUrl>* urls,
|
||||
SongList* songs, QSet<int>* song_ids) const {
|
||||
switch (item->type) {
|
||||
case LibraryItem::Type_Container: {
|
||||
const_cast<Library*>(this)->LazyPopulate(item);
|
||||
|
||||
QList<LibraryItem*> children = item->children;
|
||||
qSort(children.begin(), children.end(), boost::bind(
|
||||
&Library::CompareItems, this, _1, _2));
|
||||
|
||||
foreach (LibraryItem* child, children)
|
||||
GetChildSongs(child, urls, songs, song_ids);
|
||||
break;
|
||||
}
|
||||
|
||||
case LibraryItem::Type_Song:
|
||||
urls->append(QUrl::fromLocalFile(item->metadata.filename()));
|
||||
if (!song_ids->contains(item->metadata.id())) {
|
||||
songs->append(item->metadata);
|
||||
song_ids->insert(item->metadata.id());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SongList Library::GetChildSongs(const QModelIndex& index) const {
|
||||
QList<QUrl> dontcare;
|
||||
SongList ret;
|
||||
QSet<int> song_ids;
|
||||
|
||||
if (!index.isValid())
|
||||
return SongList();
|
||||
|
||||
GetChildSongs(IndexToItem(index), &dontcare, &ret, &song_ids);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Library::SetFilterAge(int age) {
|
||||
query_options_.max_age = age;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void Library::SetFilterText(const QString& text) {
|
||||
query_options_.filter = text;
|
||||
Reset();
|
||||
}
|
||||
|
||||
bool Library::canFetchMore(const QModelIndex &parent) const {
|
||||
if (!parent.isValid())
|
||||
return false;
|
||||
|
||||
LibraryItem* item = IndexToItem(parent);
|
||||
return !item->lazy_loaded;
|
||||
}
|
||||
|
||||
void Library::SetGroupBy(const Grouping& g) {
|
||||
group_by_ = g;
|
||||
|
||||
if (!waiting_for_threads_)
|
||||
Reset();
|
||||
|
||||
emit GroupingChanged(g);
|
||||
}
|
||||
|
||||
const Library::GroupBy& Library::Grouping::operator [](int i) const {
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
case 1: return second;
|
||||
case 2: return third;
|
||||
}
|
||||
Q_ASSERT(0);
|
||||
return first;
|
||||
}
|
||||
|
||||
Library::GroupBy& Library::Grouping::operator [](int i) {
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
case 1: return second;
|
||||
case 2: return third;
|
||||
}
|
||||
Q_ASSERT(0);
|
||||
return first;
|
||||
}
|
||||
|
181
src/library.h
181
src/library.h
@ -17,193 +17,48 @@
|
||||
#ifndef LIBRARY_H
|
||||
#define LIBRARY_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QIcon>
|
||||
|
||||
#include "backgroundthread.h"
|
||||
#include "librarybackend.h"
|
||||
#include "librarywatcher.h"
|
||||
#include "libraryquery.h"
|
||||
#include "engines/engine_fwd.h"
|
||||
#include "song.h"
|
||||
#include "libraryitem.h"
|
||||
#include "simpletreemodel.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
class LibraryDirectoryModel;
|
||||
class Database;
|
||||
class LibraryBackend;
|
||||
class LibraryModel;
|
||||
class LibraryWatcher;
|
||||
|
||||
class Library : public SimpleTreeModel<LibraryItem> {
|
||||
class Library : public QObject {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(GroupBy);
|
||||
|
||||
public:
|
||||
Library(QObject* parent = 0, const QString& table = QueryOptions::kLibraryTable);
|
||||
~Library();
|
||||
Library(Database* db, QObject* parent);
|
||||
|
||||
enum {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_ContainerType,
|
||||
Role_SortText,
|
||||
Role_Key,
|
||||
Role_Artist,
|
||||
};
|
||||
|
||||
// These values get saved in QSettings - don't change them
|
||||
enum GroupBy {
|
||||
GroupBy_None = 0,
|
||||
GroupBy_Artist = 1,
|
||||
GroupBy_Album = 2,
|
||||
GroupBy_YearAlbum = 3,
|
||||
GroupBy_Year = 4,
|
||||
GroupBy_Composer = 5,
|
||||
GroupBy_Genre = 6,
|
||||
GroupBy_AlbumArtist = 7,
|
||||
};
|
||||
|
||||
struct Grouping {
|
||||
Grouping(GroupBy f = GroupBy_None,
|
||||
GroupBy s = GroupBy_None,
|
||||
GroupBy t = GroupBy_None)
|
||||
: first(f), second(s), third(t) {}
|
||||
|
||||
GroupBy first;
|
||||
GroupBy second;
|
||||
GroupBy third;
|
||||
|
||||
const GroupBy& operator [](int i) const;
|
||||
GroupBy& operator [](int i);
|
||||
bool operator ==(const Grouping& other) const {
|
||||
return first == other.first &&
|
||||
second == other.second &&
|
||||
third == other.third;
|
||||
}
|
||||
};
|
||||
static const char* kSongsTable;
|
||||
static const char* kDirsTable;
|
||||
static const char* kSubdirsTable;
|
||||
|
||||
// Useful for tests. The library takes ownership.
|
||||
void set_backend_factory(BackgroundThreadFactory<LibraryBackendInterface>* factory);
|
||||
void set_watcher_factory(BackgroundThreadFactory<LibraryWatcher>* factory);
|
||||
|
||||
void Init();
|
||||
void StartThreads();
|
||||
|
||||
LibraryDirectoryModel* GetDirectoryModel() const { return dir_model_; }
|
||||
boost::shared_ptr<LibraryBackendInterface> GetBackend() const { return backend_->Worker(); }
|
||||
|
||||
// Get information about the library
|
||||
void GetChildSongs(LibraryItem* item, QList<QUrl>* urls, SongList* songs,
|
||||
QSet<int>* song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex& index) const;
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const;
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData* mimeData(const QModelIndexList& indexes) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
|
||||
signals:
|
||||
void Error(const QString& message);
|
||||
void TotalSongCountUpdated(int count);
|
||||
void GroupingChanged(const Library::Grouping& g);
|
||||
|
||||
void ScanStarted();
|
||||
void ScanFinished();
|
||||
|
||||
void BackendReady(boost::shared_ptr<LibraryBackendInterface> backend);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(int age);
|
||||
void SetFilterText(const QString& text);
|
||||
void SetGroupBy(const Library::Grouping& g);
|
||||
|
||||
protected:
|
||||
void LazyPopulate(LibraryItem* item) { LazyPopulate(item, false); }
|
||||
void LazyPopulate(LibraryItem* item, bool signal);
|
||||
LibraryModel* model() const { return model_; }
|
||||
|
||||
private slots:
|
||||
// From LibraryBackend
|
||||
void BackendInitialised();
|
||||
void SongsDiscovered(const SongList& songs);
|
||||
void SongsDeleted(const SongList& songs);
|
||||
void Reset();
|
||||
|
||||
// From LibraryWatcher
|
||||
void WatcherInitialised();
|
||||
|
||||
private:
|
||||
void Initialise();
|
||||
LibraryBackend* backend_;
|
||||
LibraryModel* model_;
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Library
|
||||
// constructs a database query to populate the items. Filters are added
|
||||
// for each parent item, restricting the songs returned to a particular
|
||||
// album or artist for example.
|
||||
void InitQuery(GroupBy type, LibraryQuery* q);
|
||||
void FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a
|
||||
// node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
LibraryItem* ItemFromQuery(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, const LibraryQuery& q,
|
||||
int container_level);
|
||||
LibraryItem* ItemFromSong(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, const Song& s,
|
||||
int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
LibraryItem* CreateCompilationArtistNode(bool signal, LibraryItem* parent);
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
LibraryItem* InitItem(GroupBy type, bool signal, LibraryItem* parent,
|
||||
int container_level);
|
||||
void FinishItem(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, LibraryItem* item);
|
||||
|
||||
// Functions for manipulating text
|
||||
QString TextOrUnknown(const QString& text) const;
|
||||
QString PrettyYearAlbum(int year, const QString& album) const;
|
||||
|
||||
QString SortText(QString text) const;
|
||||
QString SortTextForArtist(QString artist) const;
|
||||
QString SortTextForYear(int year) const;
|
||||
|
||||
QString DividerKey(GroupBy type, LibraryItem* item) const;
|
||||
QString DividerDisplayText(GroupBy type, const QString& key) const;
|
||||
|
||||
// Helpers
|
||||
QVariant data(const LibraryItem* item, int role) const;
|
||||
bool CompareItems(const LibraryItem* a, const LibraryItem* b) const;
|
||||
|
||||
private:
|
||||
boost::scoped_ptr<BackgroundThreadFactory<LibraryBackendInterface> > backend_factory_;
|
||||
boost::scoped_ptr<BackgroundThreadFactory<LibraryWatcher> > watcher_factory_;
|
||||
BackgroundThread<LibraryBackendInterface>* backend_;
|
||||
BackgroundThread<LibraryWatcher>* watcher_;
|
||||
LibraryDirectoryModel* dir_model_;
|
||||
|
||||
int waiting_for_threads_;
|
||||
|
||||
QueryOptions query_options_;
|
||||
Grouping group_by_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, LibraryItem*> song_nodes_;
|
||||
|
||||
// Keyed on whatever the key is for that level - artist, album, year, etc.
|
||||
QMap<QString, LibraryItem*> container_nodes_[3];
|
||||
|
||||
// Keyed on a letter, a year, a century, etc.
|
||||
QMap<QString, LibraryItem*> divider_nodes_;
|
||||
|
||||
// Only applies if the first level is "artist"
|
||||
LibraryItem* compilation_artist_node_;
|
||||
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
QIcon no_cover_icon_;
|
||||
signals:
|
||||
void ScanStarted();
|
||||
void ScanFinished();
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(Library::Grouping);
|
||||
|
||||
#endif // LIBRARY_H
|
||||
#endif
|
||||
|
@ -17,265 +17,25 @@
|
||||
#include "librarybackend.h"
|
||||
#include "libraryquery.h"
|
||||
#include "scopedtransaction.h"
|
||||
#include "database.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QVariant>
|
||||
#include <QSettings>
|
||||
#include <QtDebug>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlDriver>
|
||||
#include <QSqlQuery>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QLibrary>
|
||||
#include <QLibraryInfo>
|
||||
|
||||
using boost::shared_ptr;
|
||||
|
||||
const char* LibraryBackend::kDatabaseName = "clementine.db";
|
||||
const int LibraryBackend::kSchemaVersion = 7;
|
||||
|
||||
int (*LibraryBackend::_sqlite3_create_function) (
|
||||
sqlite3*, const char*, int, int, void*,
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*)) = NULL;
|
||||
int (*LibraryBackend::_sqlite3_value_type) (sqlite3_value*) = NULL;
|
||||
sqlite_int64 (*LibraryBackend::_sqlite3_value_int64) (sqlite3_value*) = NULL;
|
||||
const uchar* (*LibraryBackend::_sqlite3_value_text) (sqlite3_value*) = NULL;
|
||||
void (*LibraryBackend::_sqlite3_result_int64) (sqlite3_context*, sqlite_int64) = NULL;
|
||||
void* (*LibraryBackend::_sqlite3_user_data) (sqlite3_context*) = NULL;
|
||||
|
||||
bool LibraryBackend::sStaticInitDone = false;
|
||||
bool LibraryBackend::sLoadedSqliteSymbols = false;
|
||||
|
||||
|
||||
void LibraryBackend::StaticInit() {
|
||||
if (sStaticInitDone) {
|
||||
return;
|
||||
}
|
||||
sStaticInitDone = true;
|
||||
|
||||
#ifndef Q_WS_X11
|
||||
// We statically link libqsqlite.dll on windows and mac so these symbols are already
|
||||
// available
|
||||
_sqlite3_create_function = sqlite3_create_function;
|
||||
_sqlite3_value_type = sqlite3_value_type;
|
||||
_sqlite3_value_int64 = sqlite3_value_int64;
|
||||
_sqlite3_value_text = sqlite3_value_text;
|
||||
_sqlite3_result_int64 = sqlite3_result_int64;
|
||||
_sqlite3_user_data = sqlite3_user_data;
|
||||
sLoadedSqliteSymbols = true;
|
||||
return;
|
||||
#else // Q_WS_X11
|
||||
QString plugin_path = QLibraryInfo::location(QLibraryInfo::PluginsPath) +
|
||||
"/sqldrivers/libqsqlite";
|
||||
|
||||
QLibrary library(plugin_path);
|
||||
if (!library.load()) {
|
||||
qDebug() << "QLibrary::load() failed for " << plugin_path;
|
||||
return;
|
||||
}
|
||||
|
||||
_sqlite3_create_function = reinterpret_cast<Sqlite3CreateFunc>(
|
||||
library.resolve("sqlite3_create_function"));
|
||||
_sqlite3_value_type = reinterpret_cast<int (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_type"));
|
||||
_sqlite3_value_int64 = reinterpret_cast<sqlite_int64 (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_int64"));
|
||||
_sqlite3_value_text = reinterpret_cast<const uchar* (*) (sqlite3_value*)>(
|
||||
library.resolve("sqlite3_value_text"));
|
||||
_sqlite3_result_int64 = reinterpret_cast<void (*) (sqlite3_context*, sqlite_int64)>(
|
||||
library.resolve("sqlite3_result_int64"));
|
||||
_sqlite3_user_data = reinterpret_cast<void* (*) (sqlite3_context*)>(
|
||||
library.resolve("sqlite3_user_data"));
|
||||
|
||||
if (!_sqlite3_create_function ||
|
||||
!_sqlite3_value_type ||
|
||||
!_sqlite3_value_int64 ||
|
||||
!_sqlite3_value_text ||
|
||||
!_sqlite3_result_int64 ||
|
||||
!_sqlite3_user_data) {
|
||||
qDebug() << "Couldn't resolve sqlite symbols";
|
||||
sLoadedSqliteSymbols = false;
|
||||
} else {
|
||||
sLoadedSqliteSymbols = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool LibraryBackend::Like(const char* needle, const char* haystack) {
|
||||
uint hash = qHash(needle);
|
||||
if (!query_hash_ || hash != query_hash_) {
|
||||
// New query, parse and cache.
|
||||
query_cache_ = QString::fromUtf8(needle).section('%', 1, 1).split(' ');
|
||||
query_hash_ = hash;
|
||||
}
|
||||
QString b = QString::fromUtf8(haystack);
|
||||
foreach (const QString& query, query_cache_) {
|
||||
if (!b.contains(query, Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Custom LIKE(X, Y) function for sqlite3 that supports case insensitive unicode matching.
|
||||
void LibraryBackend::SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv) {
|
||||
Q_ASSERT(argc == 2 || argc == 3);
|
||||
Q_ASSERT(_sqlite3_value_type(argv[0]) == _sqlite3_value_type(argv[1]));
|
||||
|
||||
LibraryBackend* library = reinterpret_cast<LibraryBackend*>(_sqlite3_user_data(context));
|
||||
Q_ASSERT(library);
|
||||
|
||||
switch (_sqlite3_value_type(argv[0])) {
|
||||
case SQLITE_INTEGER: {
|
||||
qint64 result = _sqlite3_value_int64(argv[0]) - _sqlite3_value_int64(argv[1]);
|
||||
_sqlite3_result_int64(context, result ? 0 : 1);
|
||||
break;
|
||||
}
|
||||
case SQLITE_TEXT: {
|
||||
const char* data_a = reinterpret_cast<const char*>(_sqlite3_value_text(argv[0]));
|
||||
const char* data_b = reinterpret_cast<const char*>(_sqlite3_value_text(argv[1]));
|
||||
_sqlite3_result_int64(context, library->Like(data_a, data_b) ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LibraryBackendInterface::LibraryBackendInterface(QObject *parent)
|
||||
: QObject(parent)
|
||||
LibraryBackend::LibraryBackend(Database *db, const QString& songs_table,
|
||||
const QString& dirs_table,
|
||||
const QString& subdirs_table, QObject *parent)
|
||||
: QObject(parent),
|
||||
db_(db),
|
||||
songs_table_(songs_table),
|
||||
dirs_table_(dirs_table),
|
||||
subdirs_table_(subdirs_table)
|
||||
{
|
||||
}
|
||||
|
||||
LibraryBackend::LibraryBackend(QObject* parent, const QString& database_name)
|
||||
: LibraryBackendInterface(parent),
|
||||
injected_database_name_(database_name),
|
||||
query_hash_(0)
|
||||
{
|
||||
QSettings s;
|
||||
s.beginGroup("Library");
|
||||
directory_ = s.value("database_directory", DefaultDatabaseDirectory()).toString();
|
||||
|
||||
Connect();
|
||||
}
|
||||
|
||||
QString LibraryBackend::DefaultDatabaseDirectory() {
|
||||
QDir ret(QDir::homePath() + "/.config/" + QCoreApplication::organizationName());
|
||||
return QDir::toNativeSeparators(ret.path());
|
||||
}
|
||||
|
||||
QSqlDatabase LibraryBackend::Connect() {
|
||||
QMutexLocker l(&connect_mutex_);
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
if (!QFile::exists(directory_)) {
|
||||
QDir dir;
|
||||
if (!dir.mkpath(directory_)) {
|
||||
}
|
||||
}
|
||||
|
||||
const QString connection_id("thread_" + QString::number(
|
||||
reinterpret_cast<quint64>(QThread::currentThread())));
|
||||
|
||||
// Try to find an existing connection for this thread
|
||||
QSqlDatabase db = QSqlDatabase::database(connection_id);
|
||||
if (db.isOpen()) {
|
||||
return db;
|
||||
}
|
||||
|
||||
db = QSqlDatabase::addDatabase("QSQLITE", connection_id);
|
||||
|
||||
if (!injected_database_name_.isNull())
|
||||
db.setDatabaseName(injected_database_name_);
|
||||
else
|
||||
db.setDatabaseName(directory_ + "/" + kDatabaseName);
|
||||
|
||||
if (!db.open()) {
|
||||
emit Error("LibraryBackend: " + db.lastError().text());
|
||||
return db;
|
||||
}
|
||||
|
||||
// Find Sqlite3 functions in the Qt plugin.
|
||||
StaticInit();
|
||||
|
||||
// We want Unicode aware LIKE clauses if possible
|
||||
if (sLoadedSqliteSymbols) {
|
||||
QVariant v = db.driver()->handle();
|
||||
if (v.isValid() && qstrcmp(v.typeName(), "sqlite3*") == 0) {
|
||||
sqlite3* handle = *static_cast<sqlite3**>(v.data());
|
||||
if (handle) {
|
||||
_sqlite3_create_function(
|
||||
handle, // Sqlite3 handle.
|
||||
"LIKE", // Function name (either override or new).
|
||||
2, // Number of args.
|
||||
SQLITE_ANY, // What types this function accepts.
|
||||
this, // Custom data available via sqlite3_user_data().
|
||||
&LibraryBackend::SqliteLike, // Our function :-)
|
||||
NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (db.tables().count() == 0) {
|
||||
// Set up initial schema
|
||||
UpdateDatabaseSchema(0, db);
|
||||
}
|
||||
|
||||
// Get the database's schema version
|
||||
QSqlQuery q("SELECT version FROM schema_version", db);
|
||||
int schema_version = 0;
|
||||
if (q.next())
|
||||
schema_version = q.value(0).toInt();
|
||||
|
||||
if (schema_version > kSchemaVersion) {
|
||||
qWarning() << "The database schema (version" << schema_version << ") is newer than I was expecting";
|
||||
return db;
|
||||
}
|
||||
if (schema_version < kSchemaVersion) {
|
||||
// Update the schema
|
||||
for (int v=schema_version+1 ; v<= kSchemaVersion ; ++v) {
|
||||
UpdateDatabaseSchema(v, db);
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
void LibraryBackend::UpdateDatabaseSchema(int version, QSqlDatabase &db) {
|
||||
QString filename;
|
||||
if (version == 0)
|
||||
filename = ":/schema.sql";
|
||||
else
|
||||
filename = QString(":/schema-%1.sql").arg(version);
|
||||
|
||||
// Open and read the database schema
|
||||
QFile schema_file(filename);
|
||||
if (!schema_file.open(QIODevice::ReadOnly))
|
||||
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
|
||||
QString schema(QString::fromUtf8(schema_file.readAll()));
|
||||
|
||||
// Run each command
|
||||
QStringList commands(schema.split(";\n\n"));
|
||||
db.transaction();
|
||||
foreach (const QString& command, commands) {
|
||||
QSqlQuery query(db.exec(command));
|
||||
if (CheckErrors(query.lastError()))
|
||||
qFatal("Unable to update music library database");
|
||||
}
|
||||
db.commit();
|
||||
}
|
||||
|
||||
bool LibraryBackend::CheckErrors(const QSqlError& error) {
|
||||
if (error.isValid()) {
|
||||
qDebug() << error;
|
||||
emit Error("LibraryBackend: " + error.text());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void LibraryBackend::LoadDirectoriesAsync() {
|
||||
metaObject()->invokeMethod(this, "LoadDirectories", Qt::QueuedConnection);
|
||||
}
|
||||
@ -289,11 +49,11 @@ void LibraryBackend::UpdateCompilationsAsync() {
|
||||
}
|
||||
|
||||
void LibraryBackend::LoadDirectories() {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("SELECT ROWID, path FROM directories", db);
|
||||
QSqlQuery q(QString("SELECT ROWID, path FROM %1").arg(dirs_table_), db);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
|
||||
while (q.next()) {
|
||||
Directory dir;
|
||||
@ -305,16 +65,16 @@ void LibraryBackend::LoadDirectories() {
|
||||
}
|
||||
|
||||
SubdirectoryList LibraryBackend::SubdirsInDirectory(int id) {
|
||||
QSqlDatabase db = Connect();
|
||||
QSqlDatabase db = db_->Connect();
|
||||
return SubdirsInDirectory(id, db);
|
||||
}
|
||||
|
||||
SubdirectoryList LibraryBackend::SubdirsInDirectory(int id, QSqlDatabase &db) {
|
||||
QSqlQuery q("SELECT path, mtime FROM subdirectories"
|
||||
" WHERE directory = :dir", db);
|
||||
QSqlQuery q(QString("SELECT path, mtime FROM %1"
|
||||
" WHERE directory = :dir").arg(subdirs_table_), db);
|
||||
q.bindValue(":dir", id);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return SubdirectoryList();
|
||||
if (db_->CheckErrors(q.lastError())) return SubdirectoryList();
|
||||
|
||||
SubdirectoryList subdirs;
|
||||
while (q.next()) {
|
||||
@ -329,24 +89,24 @@ SubdirectoryList LibraryBackend::SubdirsInDirectory(int id, QSqlDatabase &db) {
|
||||
}
|
||||
|
||||
void LibraryBackend::UpdateTotalSongCount() {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("SELECT COUNT(*) FROM songs", db);
|
||||
QSqlQuery q(QString("SELECT COUNT(*) FROM %1").arg(songs_table_), db);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
if (!q.next()) return;
|
||||
|
||||
emit TotalSongCountUpdated(q.value(0).toInt());
|
||||
}
|
||||
|
||||
void LibraryBackend::AddDirectory(const QString &path) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("INSERT INTO directories (path, subdirs)"
|
||||
" VALUES (:path, 1)", db);
|
||||
QSqlQuery q(QString("INSERT INTO %1 (path, subdirs)"
|
||||
" VALUES (:path, 1)").arg(dirs_table_), db);
|
||||
q.bindValue(":path", path);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
|
||||
Directory dir;
|
||||
dir.path = path;
|
||||
@ -356,7 +116,7 @@ void LibraryBackend::AddDirectory(const QString &path) {
|
||||
}
|
||||
|
||||
void LibraryBackend::RemoveDirectory(const Directory& dir) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Remove songs first
|
||||
DeleteSongs(FindSongsInDirectory(dir.id));
|
||||
@ -364,16 +124,18 @@ void LibraryBackend::RemoveDirectory(const Directory& dir) {
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
// Delete the subdirs that were in this directory
|
||||
QSqlQuery q("DELETE FROM subdirectories WHERE directory = :id", db);
|
||||
QSqlQuery q(QString("DELETE FROM %1 WHERE directory = :id")
|
||||
.arg(subdirs_table_), db);
|
||||
q.bindValue(":id", dir.id);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
|
||||
// Now remove the directory itself
|
||||
q = QSqlQuery("DELETE FROM directories WHERE ROWID = :id", db);
|
||||
q = QSqlQuery(QString("DELETE FROM %1 WHERE ROWID = :id")
|
||||
.arg(dirs_table_), db);
|
||||
q.bindValue(":id", dir.id);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
|
||||
emit DirectoryDeleted(dir);
|
||||
|
||||
@ -381,13 +143,13 @@ void LibraryBackend::RemoveDirectory(const Directory& dir) {
|
||||
}
|
||||
|
||||
SongList LibraryBackend::FindSongsInDirectory(int id) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("SELECT ROWID, " + Song::kColumnSpec +
|
||||
" FROM songs WHERE directory = :directory", db);
|
||||
q.bindValue(":directory", id);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return SongList();
|
||||
if (db_->CheckErrors(q.lastError())) return SongList();
|
||||
|
||||
SongList ret;
|
||||
while (q.next()) {
|
||||
@ -399,15 +161,19 @@ SongList LibraryBackend::FindSongsInDirectory(int id) {
|
||||
}
|
||||
|
||||
void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlQuery find_query("SELECT ROWID FROM subdirectories"
|
||||
" WHERE directory = :id AND path = :path", db);
|
||||
QSqlQuery add_query("INSERT INTO subdirectories (directory, path, mtime)"
|
||||
" VALUES (:id, :path, :mtime)", db);
|
||||
QSqlQuery update_query("UPDATE subdirectories SET mtime = :mtime"
|
||||
" WHERE directory = :id AND path = :path", db);
|
||||
QSqlQuery delete_query("DELETE FROM subdirectories"
|
||||
" WHERE directory = :id AND path = :path", db);
|
||||
QSqlDatabase db(db_->Connect());
|
||||
QSqlQuery find_query(QString("SELECT ROWID FROM %1"
|
||||
" WHERE directory = :id AND path = :path")
|
||||
.arg(subdirs_table_), db);
|
||||
QSqlQuery add_query(QString("INSERT INTO %1 (directory, path, mtime)"
|
||||
" VALUES (:id, :path, :mtime)")
|
||||
.arg(subdirs_table_), db);
|
||||
QSqlQuery update_query(QString("UPDATE %1 SET mtime = :mtime"
|
||||
" WHERE directory = :id AND path = :path")
|
||||
.arg(subdirs_table_), db);
|
||||
QSqlQuery delete_query(QString("DELETE FROM %1"
|
||||
" WHERE directory = :id AND path = :path")
|
||||
.arg(subdirs_table_), db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
foreach (const Subdirectory& subdir, subdirs) {
|
||||
@ -416,26 +182,26 @@ void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
|
||||
delete_query.bindValue(":id", subdir.directory_id);
|
||||
delete_query.bindValue(":path", subdir.path);
|
||||
delete_query.exec();
|
||||
CheckErrors(delete_query.lastError());
|
||||
db_->CheckErrors(delete_query.lastError());
|
||||
} else {
|
||||
// See if this subdirectory already exists in the database
|
||||
find_query.bindValue(":id", subdir.directory_id);
|
||||
find_query.bindValue(":path", subdir.path);
|
||||
find_query.exec();
|
||||
if (CheckErrors(find_query.lastError())) continue;
|
||||
if (db_->CheckErrors(find_query.lastError())) continue;
|
||||
|
||||
if (find_query.next()) {
|
||||
update_query.bindValue(":mtime", subdir.mtime);
|
||||
update_query.bindValue(":id", subdir.directory_id);
|
||||
update_query.bindValue(":path", subdir.path);
|
||||
update_query.exec();
|
||||
CheckErrors(update_query.lastError());
|
||||
db_->CheckErrors(update_query.lastError());
|
||||
} else {
|
||||
add_query.bindValue(":id", subdir.directory_id);
|
||||
add_query.bindValue(":path", subdir.path);
|
||||
add_query.bindValue(":mtime", subdir.mtime);
|
||||
add_query.exec();
|
||||
CheckErrors(add_query.lastError());
|
||||
db_->CheckErrors(add_query.lastError());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -443,16 +209,15 @@ void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
|
||||
}
|
||||
|
||||
void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery check_dir(
|
||||
"SELECT ROWID FROM directories WHERE ROWID = :id", db);
|
||||
QSqlQuery add_song(
|
||||
"INSERT INTO songs (" + Song::kColumnSpec + ")"
|
||||
" VALUES (" + Song::kBindSpec + ")", db);
|
||||
QSqlQuery update_song(
|
||||
"UPDATE songs SET " + Song::kUpdateSpec +
|
||||
" WHERE ROWID = :id", db);
|
||||
QSqlQuery check_dir(QString("SELECT ROWID FROM %1 WHERE ROWID = :id")
|
||||
.arg(dirs_table_), db);
|
||||
QSqlQuery add_song(QString("INSERT INTO %1 (" + Song::kColumnSpec + ")"
|
||||
" VALUES (" + Song::kBindSpec + ")")
|
||||
.arg(songs_table_), db);
|
||||
QSqlQuery update_song(QString("UPDATE %1 SET " + Song::kUpdateSpec +
|
||||
" WHERE ROWID = :id").arg(songs_table_), db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
@ -465,7 +230,7 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
|
||||
// while LibraryWatcher is scanning it.
|
||||
check_dir.bindValue(":id", song.directory_id());
|
||||
check_dir.exec();
|
||||
if (CheckErrors(check_dir.lastError())) continue;
|
||||
if (db_->CheckErrors(check_dir.lastError())) continue;
|
||||
|
||||
if (!check_dir.next())
|
||||
continue; // Directory didn't exist
|
||||
@ -475,7 +240,7 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
|
||||
// Create
|
||||
song.BindToQuery(&add_song);
|
||||
add_song.exec();
|
||||
if (CheckErrors(add_song.lastError())) continue;
|
||||
if (db_->CheckErrors(add_song.lastError())) continue;
|
||||
|
||||
Song copy(song);
|
||||
copy.set_id(add_song.lastInsertId().toInt());
|
||||
@ -490,7 +255,7 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
|
||||
song.BindToQuery(&update_song);
|
||||
update_song.bindValue(":id", song.id());
|
||||
update_song.exec();
|
||||
if (CheckErrors(update_song.lastError())) continue;
|
||||
if (db_->CheckErrors(update_song.lastError())) continue;
|
||||
|
||||
deleted_songs << old_song;
|
||||
added_songs << song;
|
||||
@ -509,30 +274,32 @@ void LibraryBackend::AddOrUpdateSongs(const SongList& songs) {
|
||||
}
|
||||
|
||||
void LibraryBackend::UpdateMTimesOnly(const SongList& songs) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("UPDATE songs SET mtime = :mtime WHERE ROWID = :id", db);
|
||||
QSqlQuery q(QString("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id")
|
||||
.arg(songs_table_), db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
foreach (const Song& song, songs) {
|
||||
q.bindValue(":mtime", song.mtime());
|
||||
q.bindValue(":id", song.id());
|
||||
q.exec();
|
||||
CheckErrors(q.lastError());
|
||||
db_->CheckErrors(q.lastError());
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
void LibraryBackend::DeleteSongs(const SongList &songs) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("DELETE FROM songs WHERE ROWID = :id", db);
|
||||
QSqlQuery q(QString("DELETE FROM %1 WHERE ROWID = :id")
|
||||
.arg(songs_table_), db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
foreach (const Song& song, songs) {
|
||||
q.bindValue(":id", song.id());
|
||||
q.exec();
|
||||
CheckErrors(q.lastError());
|
||||
db_->CheckErrors(q.lastError());
|
||||
}
|
||||
transaction.Commit();
|
||||
|
||||
@ -583,13 +350,13 @@ SongList LibraryBackend::GetSongs(const QString& artist, const QString& album, c
|
||||
}
|
||||
|
||||
Song LibraryBackend::GetSongById(int id) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q("SELECT ROWID, " + Song::kColumnSpec + " FROM songs"
|
||||
" WHERE ROWID = :id", db);
|
||||
QSqlQuery q(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1"
|
||||
" WHERE ROWID = :id").arg(songs_table_), db);
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return Song();
|
||||
if (db_->CheckErrors(q.lastError())) return Song();
|
||||
|
||||
q.next();
|
||||
|
||||
@ -630,14 +397,14 @@ SongList LibraryBackend::GetCompilationSongs(const QString& album, const QueryOp
|
||||
}
|
||||
|
||||
void LibraryBackend::UpdateCompilations() {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Look for albums that have songs by more than one artist in the same
|
||||
// directory
|
||||
|
||||
QSqlQuery q("SELECT artist, album, filename, sampler FROM songs ORDER BY album", db);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError())) return;
|
||||
if (db_->CheckErrors(q.lastError())) return;
|
||||
|
||||
QMap<QString, CompilationInfo> compilation_info;
|
||||
while (q.next()) {
|
||||
@ -668,12 +435,13 @@ void LibraryBackend::UpdateCompilations() {
|
||||
}
|
||||
|
||||
// Now mark the songs that we think are in compilations
|
||||
QSqlQuery update("UPDATE songs"
|
||||
" SET sampler = :sampler,"
|
||||
" effective_compilation = ((compilation OR :sampler OR forced_compilation_on) AND NOT forced_compilation_off) + 0"
|
||||
" WHERE album = :album", db);
|
||||
QSqlQuery find_songs("SELECT ROWID, " + Song::kColumnSpec + " FROM songs"
|
||||
" WHERE album = :album AND sampler = :sampler", db);
|
||||
QSqlQuery update(QString("UPDATE %1"
|
||||
" SET sampler = :sampler,"
|
||||
" effective_compilation = ((compilation OR :sampler OR forced_compilation_on) AND NOT forced_compilation_off) + 0"
|
||||
" WHERE album = :album").arg(songs_table_), db);
|
||||
QSqlQuery find_songs(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1"
|
||||
" WHERE album = :album AND sampler = :sampler")
|
||||
.arg(songs_table_), db);
|
||||
|
||||
SongList deleted_songs;
|
||||
SongList added_songs;
|
||||
@ -725,7 +493,7 @@ void LibraryBackend::UpdateCompilations(QSqlQuery& find_songs, QSqlQuery& update
|
||||
update.bindValue(":sampler", sampler);
|
||||
update.bindValue(":album", album);
|
||||
update.exec();
|
||||
CheckErrors(update.lastError());
|
||||
db_->CheckErrors(update.lastError());
|
||||
}
|
||||
|
||||
LibraryBackend::AlbumList LibraryBackend::GetAlbums(const QString& artist,
|
||||
@ -801,10 +569,10 @@ void LibraryBackend::UpdateManualAlbumArtAsync(const QString &artist,
|
||||
void LibraryBackend::UpdateManualAlbumArt(const QString &artist,
|
||||
const QString &album,
|
||||
const QString &art) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QString sql("UPDATE songs SET art_manual = :art"
|
||||
" WHERE album = :album");
|
||||
QString sql(QString("UPDATE %1 SET art_manual = :art"
|
||||
" WHERE album = :album").arg(songs_table_));
|
||||
if (!artist.isNull())
|
||||
sql += " AND artist = :artist";
|
||||
|
||||
@ -815,11 +583,11 @@ void LibraryBackend::UpdateManualAlbumArt(const QString &artist,
|
||||
q.bindValue(":artist", artist);
|
||||
|
||||
q.exec();
|
||||
CheckErrors(q.lastError());
|
||||
db_->CheckErrors(q.lastError());
|
||||
}
|
||||
|
||||
void LibraryBackend::ForceCompilation(const QString& artist, const QString& album, bool on) {
|
||||
QSqlDatabase db(Connect());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
// Get the songs before they're updated
|
||||
LibraryQuery query;
|
||||
@ -838,10 +606,10 @@ void LibraryBackend::ForceCompilation(const QString& artist, const QString& albu
|
||||
}
|
||||
|
||||
// Update the songs
|
||||
QString sql("UPDATE songs SET forced_compilation_on = :forced_compilation_on,"
|
||||
" forced_compilation_off = :forced_compilation_off,"
|
||||
" effective_compilation = ((compilation OR sampler OR :forced_compilation_on) AND NOT :forced_compilation_off) + 0"
|
||||
" WHERE album = :album");
|
||||
QString sql(QString("UPDATE %1 SET forced_compilation_on = :forced_compilation_on,"
|
||||
" forced_compilation_off = :forced_compilation_off,"
|
||||
" effective_compilation = ((compilation OR sampler OR :forced_compilation_on) AND NOT :forced_compilation_off) + 0"
|
||||
" WHERE album = :album").arg(songs_table_));
|
||||
if (!artist.isEmpty())
|
||||
sql += " AND artist = :artist";
|
||||
|
||||
@ -853,7 +621,7 @@ void LibraryBackend::ForceCompilation(const QString& artist, const QString& albu
|
||||
q.bindValue(":artist", artist);
|
||||
|
||||
q.exec();
|
||||
CheckErrors(q.lastError());
|
||||
db_->CheckErrors(q.lastError());
|
||||
|
||||
// Now get the updated songs
|
||||
if (!ExecQuery(&query)) return;
|
||||
@ -872,78 +640,5 @@ void LibraryBackend::ForceCompilation(const QString& artist, const QString& albu
|
||||
}
|
||||
|
||||
bool LibraryBackend::ExecQuery(LibraryQuery *q) {
|
||||
return !CheckErrors(q->Exec(Connect()));
|
||||
}
|
||||
|
||||
LibraryBackendInterface::PlaylistList LibraryBackend::GetAllPlaylists() {
|
||||
qWarning() << "Not implemented:" << __PRETTY_FUNCTION__;
|
||||
return PlaylistList();
|
||||
}
|
||||
|
||||
PlaylistItemList LibraryBackend::GetPlaylistItems(int playlist) {
|
||||
QSqlDatabase db(Connect());
|
||||
|
||||
PlaylistItemList ret;
|
||||
|
||||
QSqlQuery q("SELECT songs.ROWID, " + Song::kJoinSpec + ","
|
||||
" p.type, p.url, p.title, p.artist, p.album, p.length,"
|
||||
" p.radio_service"
|
||||
" FROM playlist_items AS p"
|
||||
" LEFT JOIN songs"
|
||||
" ON p.library_id = songs.ROWID"
|
||||
" WHERE p.playlist = :playlist", db);
|
||||
q.bindValue(":playlist", playlist);
|
||||
q.exec();
|
||||
if (CheckErrors(q.lastError()))
|
||||
return ret;
|
||||
|
||||
while (q.next()) {
|
||||
// The song table gets joined first, plus one for the song ROWID
|
||||
const int row = Song::kColumns.count() + 1;
|
||||
|
||||
shared_ptr<PlaylistItem> item(
|
||||
PlaylistItem::NewFromType(q.value(row + 0).toString()));
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
if (item->InitFromQuery(q))
|
||||
ret << item;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LibraryBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items) {
|
||||
metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection,
|
||||
Q_ARG(int, playlist),
|
||||
Q_ARG(PlaylistItemList, items));
|
||||
}
|
||||
|
||||
void LibraryBackend::SavePlaylist(int playlist, const PlaylistItemList& items) {
|
||||
QSqlDatabase db(Connect());
|
||||
|
||||
QSqlQuery clear("DELETE FROM playlist_items WHERE playlist = :playlist", db);
|
||||
QSqlQuery insert("INSERT INTO playlist_items"
|
||||
" (playlist, type, library_id, url, title, artist, album,"
|
||||
" length, radio_service)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
// Clear the existing items in the playlist
|
||||
clear.bindValue(":playlist", playlist);
|
||||
clear.exec();
|
||||
if (CheckErrors(clear.lastError()))
|
||||
return;
|
||||
|
||||
// Save the new ones
|
||||
foreach (shared_ptr<PlaylistItem> item, items) {
|
||||
insert.bindValue(0, playlist);
|
||||
item->BindToQuery(&insert);
|
||||
|
||||
insert.exec();
|
||||
CheckErrors(insert.lastError());
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
return !db_->CheckErrors(q->Exec(db_->Connect(), songs_table_));
|
||||
}
|
||||
|
@ -18,25 +18,21 @@
|
||||
#define LIBRARYBACKEND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSqlError>
|
||||
#include <QSqlDatabase>
|
||||
#include <QMutex>
|
||||
#include <QSet>
|
||||
|
||||
#include "directory.h"
|
||||
#include "song.h"
|
||||
#include "libraryquery.h"
|
||||
#include "playlistitem.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
class Database;
|
||||
|
||||
#include "gtest/gtest_prod.h"
|
||||
|
||||
class LibraryBackendInterface : public QObject {
|
||||
class LibraryBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LibraryBackendInterface(QObject* parent = 0);
|
||||
LibraryBackend(Database* db, const QString& songs_table,
|
||||
const QString& dirs_table, const QString& subdirs_table,
|
||||
QObject* parent = 0);
|
||||
|
||||
struct Album {
|
||||
Album() {}
|
||||
@ -53,88 +49,6 @@ class LibraryBackendInterface : public QObject {
|
||||
};
|
||||
typedef QList<Album> AlbumList;
|
||||
|
||||
struct Playlist {
|
||||
int id;
|
||||
QString name;
|
||||
};
|
||||
typedef QList<Playlist> PlaylistList;
|
||||
|
||||
virtual void Stop() {};
|
||||
|
||||
// Get a list of directories in the library. Emits DirectoriesDiscovered.
|
||||
virtual void LoadDirectoriesAsync() = 0;
|
||||
|
||||
// Counts the songs in the library. Emits TotalSongCountUpdated
|
||||
virtual void UpdateTotalSongCountAsync() = 0;
|
||||
|
||||
// Functions for getting songs
|
||||
virtual SongList FindSongsInDirectory(int id) = 0;
|
||||
virtual SubdirectoryList SubdirsInDirectory(int id) = 0;
|
||||
|
||||
virtual QStringList GetAllArtists(const QueryOptions& opt = QueryOptions()) = 0;
|
||||
virtual SongList GetSongs(const QString& artist, const QString& album, const QueryOptions& opt = QueryOptions()) = 0;
|
||||
|
||||
virtual bool HasCompilations(const QueryOptions& opt = QueryOptions()) = 0;
|
||||
virtual SongList GetCompilationSongs(const QString& album, const QueryOptions& opt = QueryOptions()) = 0;
|
||||
|
||||
virtual AlbumList GetAllAlbums(const QueryOptions& opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetAlbumsByArtist(const QString& artist, const QueryOptions& opt = QueryOptions()) = 0;
|
||||
virtual AlbumList GetCompilationAlbums(const QueryOptions& opt = QueryOptions()) = 0;
|
||||
|
||||
virtual void UpdateManualAlbumArtAsync(const QString& artist, const QString& album, const QString& art) = 0;
|
||||
virtual Album GetAlbumArt(const QString& artist, const QString& album) = 0;
|
||||
|
||||
virtual Song GetSongById(int id) = 0;
|
||||
|
||||
virtual bool ExecQuery(LibraryQuery* q) = 0;
|
||||
|
||||
// Add or remove directories to the library
|
||||
virtual void AddDirectory(const QString& path) = 0;
|
||||
virtual void RemoveDirectory(const Directory& dir) = 0;
|
||||
|
||||
// Update compilation flags on songs
|
||||
virtual void UpdateCompilationsAsync() = 0;
|
||||
|
||||
// Functions for getting playlists
|
||||
virtual PlaylistList GetAllPlaylists() = 0;
|
||||
virtual PlaylistItemList GetPlaylistItems(int playlist) = 0;
|
||||
virtual void SavePlaylistAsync(int playlist, const PlaylistItemList& items) = 0;
|
||||
|
||||
public slots:
|
||||
virtual void LoadDirectories() = 0;
|
||||
virtual void UpdateTotalSongCount() = 0;
|
||||
virtual void AddOrUpdateSongs(const SongList& songs) = 0;
|
||||
virtual void UpdateMTimesOnly(const SongList& songs) = 0;
|
||||
virtual void DeleteSongs(const SongList& songs) = 0;
|
||||
virtual void AddOrUpdateSubdirs(const SubdirectoryList& subdirs) = 0;
|
||||
virtual void UpdateCompilations() = 0;
|
||||
virtual void UpdateManualAlbumArt(const QString& artist, const QString& album, const QString& art) = 0;
|
||||
virtual void ForceCompilation(const QString& artist, const QString& album, bool on) = 0;
|
||||
virtual void SavePlaylist(int playlist, const PlaylistItemList& items) = 0;
|
||||
|
||||
signals:
|
||||
void Error(const QString& message);
|
||||
|
||||
void DirectoryDiscovered(const Directory& dir, const SubdirectoryList& subdirs);
|
||||
void DirectoryDeleted(const Directory& dir);
|
||||
|
||||
void SongsDiscovered(const SongList& songs);
|
||||
void SongsDeleted(const SongList& songs);
|
||||
|
||||
void TotalSongCountUpdated(int total);
|
||||
};
|
||||
|
||||
|
||||
class LibraryBackend : public LibraryBackendInterface {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LibraryBackend(QObject* parent = 0, const QString& database_name = QString());
|
||||
|
||||
static const int kSchemaVersion;
|
||||
|
||||
// This actually refers to the location of the sqlite database
|
||||
static QString DefaultDatabaseDirectory();
|
||||
|
||||
// Get a list of directories in the library. Emits DirectoriesDiscovered.
|
||||
void LoadDirectoriesAsync();
|
||||
@ -167,10 +81,6 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
|
||||
bool ExecQuery(LibraryQuery* q);
|
||||
|
||||
PlaylistList GetAllPlaylists();
|
||||
PlaylistItemList GetPlaylistItems(int playlist);
|
||||
void SavePlaylistAsync(int playlist, const PlaylistItemList& items);
|
||||
|
||||
public slots:
|
||||
void LoadDirectories();
|
||||
void UpdateTotalSongCount();
|
||||
@ -181,7 +91,15 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
void UpdateCompilations();
|
||||
void UpdateManualAlbumArt(const QString& artist, const QString& album, const QString& art);
|
||||
void ForceCompilation(const QString& artist, const QString& album, bool on);
|
||||
void SavePlaylist(int playlist, const PlaylistItemList& items);
|
||||
|
||||
signals:
|
||||
void DirectoryDiscovered(const Directory& dir, const SubdirectoryList& subdirs);
|
||||
void DirectoryDeleted(const Directory& dir);
|
||||
|
||||
void SongsDiscovered(const SongList& songs);
|
||||
void SongsDeleted(const SongList& songs);
|
||||
|
||||
void TotalSongCountUpdated(int total);
|
||||
|
||||
private:
|
||||
struct CompilationInfo {
|
||||
@ -194,10 +112,6 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
bool has_not_samplers;
|
||||
};
|
||||
|
||||
QSqlDatabase Connect();
|
||||
void UpdateDatabaseSchema(int version, QSqlDatabase& db);
|
||||
bool CheckErrors(const QSqlError& error);
|
||||
|
||||
void UpdateCompilations(QSqlQuery& find_songs, QSqlQuery& update,
|
||||
SongList& deleted_songs, SongList& added_songs,
|
||||
const QString& album, int sampler);
|
||||
@ -206,53 +120,10 @@ class LibraryBackend : public LibraryBackendInterface {
|
||||
SubdirectoryList SubdirsInDirectory(int id, QSqlDatabase& db);
|
||||
|
||||
private:
|
||||
static const char* kDatabaseName;
|
||||
|
||||
QString directory_;
|
||||
QMutex connect_mutex_;
|
||||
|
||||
// Used by tests
|
||||
QString injected_database_name_;
|
||||
|
||||
|
||||
uint query_hash_;
|
||||
QStringList query_cache_;
|
||||
|
||||
FRIEND_TEST(LibraryBackendTest, LikeWorksWithAllAscii);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeWorksWithUnicode);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeAsciiCaseInsensitive);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeUnicodeCaseInsensitive);
|
||||
FRIEND_TEST(LibraryBackendTest, LikePerformance);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeCacheInvalidated);
|
||||
FRIEND_TEST(LibraryBackendTest, LikeQuerySplit);
|
||||
|
||||
// Do static initialisation like loading sqlite functions.
|
||||
static void StaticInit();
|
||||
// Custom LIKE() function for sqlite.
|
||||
bool Like(const char* needle, const char* haystack);
|
||||
static void SqliteLike(sqlite3_context* context, int argc, sqlite3_value** argv);
|
||||
typedef int (*Sqlite3CreateFunc) (
|
||||
sqlite3*, const char*, int, int, void*,
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*, int, sqlite3_value**),
|
||||
void (*) (sqlite3_context*));
|
||||
|
||||
// Sqlite3 functions. These will be loaded from the sqlite3 plugin.
|
||||
static Sqlite3CreateFunc _sqlite3_create_function;
|
||||
static int (*_sqlite3_value_type) (sqlite3_value*);
|
||||
static sqlite_int64 (*_sqlite3_value_int64) (sqlite3_value*);
|
||||
static const uchar* (*_sqlite3_value_text) (sqlite3_value*);
|
||||
static void (*_sqlite3_result_int64) (sqlite3_context*, sqlite_int64);
|
||||
static void* (*_sqlite3_user_data) (sqlite3_context*);
|
||||
|
||||
static bool sStaticInitDone;
|
||||
static bool sLoadedSqliteSymbols;
|
||||
};
|
||||
|
||||
class MemoryLibraryBackend : public LibraryBackend {
|
||||
public:
|
||||
MemoryLibraryBackend(QObject* parent = 0)
|
||||
: LibraryBackend(parent, ":memory:") {}
|
||||
Database* db_;
|
||||
QString songs_table_;
|
||||
QString dirs_table_;
|
||||
QString subdirs_table_;
|
||||
};
|
||||
|
||||
#endif // LIBRARYBACKEND_H
|
||||
|
@ -41,12 +41,6 @@ void LibraryConfig::SetModel(LibraryDirectoryModel *model) {
|
||||
connect(ui_.list->selectionModel(),
|
||||
SIGNAL(currentRowChanged(QModelIndex, QModelIndex)),
|
||||
SLOT(CurrentRowChanged(QModelIndex)));
|
||||
|
||||
|
||||
if (model_->IsBackendReady())
|
||||
BackendReady();
|
||||
else
|
||||
connect(model_, SIGNAL(BackendReady()), SLOT(BackendReady()));
|
||||
}
|
||||
|
||||
void LibraryConfig::Add() {
|
||||
@ -71,12 +65,6 @@ void LibraryConfig::CurrentRowChanged(const QModelIndex& index) {
|
||||
ui_.remove->setEnabled(index.isValid());
|
||||
}
|
||||
|
||||
void LibraryConfig::BackendReady() {
|
||||
ui_.list->setEnabled(true);
|
||||
ui_.add->setEnabled(true);
|
||||
ui_.remove->setEnabled(true);
|
||||
}
|
||||
|
||||
void LibraryConfig::Save() {
|
||||
QSettings s;
|
||||
s.beginGroup(LibraryView::kSettingsGroup);
|
||||
|
@ -41,7 +41,6 @@ class LibraryConfig : public QWidget {
|
||||
private slots:
|
||||
void Add();
|
||||
void Remove();
|
||||
void BackendReady();
|
||||
|
||||
void CurrentRowChanged(const QModelIndex& index);
|
||||
|
||||
|
@ -24,24 +24,17 @@
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QListView" name="list">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QListView" name="list"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="add">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add new folder...</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../data/data.qrc">
|
||||
<iconset>
|
||||
<normaloff>:/folder-new.png</normaloff>:/folder-new.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
@ -57,9 +50,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove folder</string>
|
||||
</property>
|
||||
@ -111,8 +101,6 @@
|
||||
<tabstop>add</tabstop>
|
||||
<tabstop>remove</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../data/data.qrc"/>
|
||||
</resources>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -17,22 +17,13 @@
|
||||
#include "librarydirectorymodel.h"
|
||||
#include "librarybackend.h"
|
||||
|
||||
LibraryDirectoryModel::LibraryDirectoryModel(QObject* parent)
|
||||
LibraryDirectoryModel::LibraryDirectoryModel(LibraryBackend* backend, QObject* parent)
|
||||
: QStandardItemModel(parent),
|
||||
dir_icon_(":folder.png")
|
||||
dir_icon_(":folder.png"),
|
||||
backend_(backend)
|
||||
{
|
||||
}
|
||||
|
||||
void LibraryDirectoryModel::SetBackend(boost::shared_ptr<LibraryBackendInterface> backend) {
|
||||
if (backend_)
|
||||
backend_->disconnect(this);
|
||||
|
||||
backend_ = backend;
|
||||
|
||||
connect(backend_.get(), SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), SLOT(DirectoryDiscovered(Directory)));
|
||||
connect(backend_.get(), SIGNAL(DirectoryDeleted(Directory)), SLOT(DirectoryDeleted(Directory)));
|
||||
|
||||
emit BackendReady();
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory, SubdirectoryList)), SLOT(DirectoryDiscovered(Directory)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), SLOT(DirectoryDeleted(Directory)));
|
||||
}
|
||||
|
||||
void LibraryDirectoryModel::DirectoryDiscovered(const Directory &dir) {
|
||||
|
@ -20,28 +20,20 @@
|
||||
#include <QStandardItemModel>
|
||||
#include <QIcon>
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include "directory.h"
|
||||
|
||||
class LibraryBackendInterface;
|
||||
class LibraryBackend;
|
||||
|
||||
class LibraryDirectoryModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LibraryDirectoryModel(QObject* parent = 0);
|
||||
|
||||
void SetBackend(boost::shared_ptr<LibraryBackendInterface> backend);
|
||||
bool IsBackendReady() const { return backend_; }
|
||||
LibraryDirectoryModel(LibraryBackend* backend, QObject* parent = 0);
|
||||
|
||||
// To be called by GUIs
|
||||
void AddDirectory(const QString& path);
|
||||
void RemoveDirectory(const QModelIndex& index);
|
||||
|
||||
signals:
|
||||
void BackendReady();
|
||||
|
||||
private slots:
|
||||
// To be called by the backend
|
||||
void DirectoryDiscovered(const Directory& directories);
|
||||
@ -51,7 +43,7 @@ class LibraryDirectoryModel : public QStandardItemModel {
|
||||
static const int kIdRole = Qt::UserRole + 1;
|
||||
|
||||
QIcon dir_icon_;
|
||||
boost::shared_ptr<LibraryBackendInterface> backend_;
|
||||
LibraryBackend* backend_;
|
||||
};
|
||||
|
||||
#endif // LIBRARYDIRECTORYMODEL_H
|
||||
|
783
src/librarymodel.cpp
Normal file
783
src/librarymodel.cpp
Normal file
@ -0,0 +1,783 @@
|
||||
/* 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 "librarymodel.h"
|
||||
#include "librarybackend.h"
|
||||
#include "libraryitem.h"
|
||||
#include "songmimedata.h"
|
||||
#include "librarydirectorymodel.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QMetaEnum>
|
||||
|
||||
#include <boost/bind.hpp>
|
||||
|
||||
|
||||
LibraryModel::LibraryModel(LibraryBackend* backend, QObject* parent)
|
||||
: SimpleTreeModel<LibraryItem>(new LibraryItem(this), parent),
|
||||
backend_(backend),
|
||||
dir_model_(new LibraryDirectoryModel(backend, this)),
|
||||
artist_icon_(":artist.png"),
|
||||
album_icon_(":album.png"),
|
||||
no_cover_icon_(":nocover.png")
|
||||
{
|
||||
root_->lazy_loaded = true;
|
||||
|
||||
group_by_[0] = GroupBy_Artist;
|
||||
group_by_[1] = GroupBy_Album;
|
||||
group_by_[2] = GroupBy_None;
|
||||
}
|
||||
|
||||
LibraryModel::~LibraryModel() {
|
||||
delete root_;
|
||||
}
|
||||
|
||||
void LibraryModel::Init() {
|
||||
connect(backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
|
||||
connect(backend_, SIGNAL(SongsDeleted(SongList)), SLOT(SongsDeleted(SongList)));
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SIGNAL(TotalSongCountUpdated(int)));
|
||||
|
||||
backend_->UpdateTotalSongCountAsync();
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
void LibraryModel::SongsDiscovered(const SongList& songs) {
|
||||
foreach (const Song& song, songs) {
|
||||
// Sanity check to make sure we don't add songs that are outside the user's
|
||||
// filter
|
||||
if (!query_options_.Matches(song))
|
||||
continue;
|
||||
|
||||
// Hey, we've already got that one!
|
||||
if (song_nodes_.contains(song.id()))
|
||||
continue;
|
||||
|
||||
// Before we can add each song we need to make sure the required container
|
||||
// items already exist in the tree. These depend on which "group by"
|
||||
// settings the user has on the library. Eg. if the user grouped by
|
||||
// artist and album, we would need to make sure nodes for the song's artist
|
||||
// and album were already in the tree.
|
||||
|
||||
// Find parent containers in the tree
|
||||
LibraryItem* container = root_;
|
||||
for (int i=0 ; i<3 ; ++i) {
|
||||
GroupBy type = group_by_[i];
|
||||
if (type == GroupBy_None) break;
|
||||
|
||||
// Special case: if we're at the top level and the song is a compilation
|
||||
// and the top level is Artists, then we want the Various Artists node :(
|
||||
if (i == 0 && type == GroupBy_Artist && song.is_compilation()) {
|
||||
if (compilation_artist_node_ == NULL)
|
||||
CreateCompilationArtistNode(true, root_);
|
||||
container = compilation_artist_node_;
|
||||
} else {
|
||||
// Otherwise find the proper container at this level based on the
|
||||
// item's key
|
||||
QString key;
|
||||
switch (type) {
|
||||
case GroupBy_Album: key = song.album(); break;
|
||||
case GroupBy_Artist: key = song.artist(); break;
|
||||
case GroupBy_Composer: key = song.composer(); break;
|
||||
case GroupBy_Genre: key = song.genre(); break;
|
||||
case GroupBy_AlbumArtist: key = song.albumartist(); break;
|
||||
case GroupBy_Year:
|
||||
key = QString::number(qMax(0, song.year())); break;
|
||||
case GroupBy_YearAlbum:
|
||||
key = PrettyYearAlbum(qMax(0, song.year()), song.album()); break;
|
||||
case GroupBy_None: Q_ASSERT(0); break;
|
||||
}
|
||||
|
||||
// Does it exist already?
|
||||
if (!container_nodes_[i].contains(key)) {
|
||||
// Create the container
|
||||
container_nodes_[i][key] =
|
||||
ItemFromSong(type, true, i == 0, container, song, i);
|
||||
}
|
||||
container = container_nodes_[i][key];
|
||||
}
|
||||
|
||||
// If we just created the damn thing then we don't need to continue into
|
||||
// it any further because it'll get lazy-loaded properly later.
|
||||
if (!container->lazy_loaded)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!container->lazy_loaded)
|
||||
continue;
|
||||
|
||||
// We've gone all the way down to the deepest level and everything was
|
||||
// already lazy loaded, so now we have to create the song in the container.
|
||||
song_nodes_[song.id()] =
|
||||
ItemFromSong(GroupBy_None, true, false, container, song, -1);
|
||||
}
|
||||
}
|
||||
|
||||
LibraryItem* LibraryModel::CreateCompilationArtistNode(bool signal, LibraryItem* parent) {
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent), parent->children.count(), parent->children.count());
|
||||
|
||||
compilation_artist_node_ =
|
||||
new LibraryItem(LibraryItem::Type_Container, parent);
|
||||
compilation_artist_node_->key = tr("Various Artists");
|
||||
compilation_artist_node_->sort_text = " various";
|
||||
compilation_artist_node_->container_level = parent->container_level + 1;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
|
||||
return compilation_artist_node_;
|
||||
}
|
||||
|
||||
QString LibraryModel::DividerKey(GroupBy type, LibraryItem* item) const {
|
||||
// Items which are to be grouped under the same divider must produce the
|
||||
// same divider key. This will only get called for top-level items.
|
||||
|
||||
if (item->sort_text.isEmpty())
|
||||
return QString();
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_Artist:
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_AlbumArtist:
|
||||
if (item->sort_text[0].isDigit())
|
||||
return "0";
|
||||
if (item->sort_text[0] == ' ')
|
||||
return QString();
|
||||
return QString(item->sort_text[0]);
|
||||
|
||||
case GroupBy_Year:
|
||||
return SortTextForYear(item->sort_text.toInt() / 10 * 10);
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
return SortTextForYear(item->metadata.year());
|
||||
|
||||
case GroupBy_None:
|
||||
default:
|
||||
Q_ASSERT(0);
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
QString LibraryModel::DividerDisplayText(GroupBy type, const QString& key) const {
|
||||
// Pretty display text for the dividers.
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_Artist:
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_AlbumArtist:
|
||||
if (key == "0")
|
||||
return "0-9";
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
if (key == "0000")
|
||||
return tr("Unknown");
|
||||
return key.toUpper();
|
||||
|
||||
case GroupBy_Year:
|
||||
if (key == "0000")
|
||||
return tr("Unknown");
|
||||
return QString::number(key.toInt()); // To remove leading 0s
|
||||
|
||||
case GroupBy_None:
|
||||
default:
|
||||
Q_ASSERT(0);
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryModel::SongsDeleted(const SongList& songs) {
|
||||
// Delete the actual song nodes first, keeping track of each parent so we
|
||||
// might check to see if they're empty later.
|
||||
QSet<LibraryItem*> parents;
|
||||
foreach (const Song& song, songs) {
|
||||
if (song_nodes_.contains(song.id())) {
|
||||
LibraryItem* node = song_nodes_[song.id()];
|
||||
|
||||
if (node->parent != root_)
|
||||
parents << node->parent;
|
||||
|
||||
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
||||
node->parent->Delete(node->row);
|
||||
song_nodes_.remove(song.id());
|
||||
endRemoveRows();
|
||||
} else {
|
||||
// If we get here it means some of the songs we want to delete haven't
|
||||
// been lazy-loaded yet. This is bad, because it would mean that to
|
||||
// clean up empty parents we would need to lazy-load them all
|
||||
// individually to see if they're empty. This can take a very long time,
|
||||
// so better to just reset the model and be done with it.
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Now delete empty parents
|
||||
QSet<QString> divider_keys;
|
||||
while (!parents.isEmpty()) {
|
||||
foreach (LibraryItem* node, parents) {
|
||||
parents.remove(node);
|
||||
if (node->children.count() != 0)
|
||||
continue;
|
||||
|
||||
// Consider its parent for the next round
|
||||
if (node->parent != root_)
|
||||
parents << node->parent;
|
||||
|
||||
// Maybe consider its divider node
|
||||
if (node->container_level == 0)
|
||||
divider_keys << DividerKey(group_by_[0], node);
|
||||
|
||||
// Special case the Various Artists node
|
||||
if (node == compilation_artist_node_)
|
||||
compilation_artist_node_ = NULL;
|
||||
else
|
||||
container_nodes_[node->container_level].remove(node->key);
|
||||
|
||||
// It was empty - delete it
|
||||
beginRemoveRows(ItemToIndex(node->parent), node->row, node->row);
|
||||
node->parent->Delete(node->row);
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
|
||||
// Delete empty dividers
|
||||
foreach (const QString& divider_key, divider_keys) {
|
||||
if (!divider_nodes_.contains(divider_key))
|
||||
continue;
|
||||
|
||||
// Look to see if there are any other items still under this divider
|
||||
bool found = false;
|
||||
foreach (LibraryItem* node, container_nodes_[0].values()) {
|
||||
if (DividerKey(group_by_[0], node) == divider_key) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
continue;
|
||||
|
||||
// Remove the divider
|
||||
int row = divider_nodes_[divider_key]->row;
|
||||
beginRemoveRows(ItemToIndex(root_), row, row);
|
||||
root_->Delete(row);
|
||||
endRemoveRows();
|
||||
divider_nodes_.remove(divider_key);
|
||||
}
|
||||
}
|
||||
|
||||
QVariant LibraryModel::data(const QModelIndex& index, int role) const {
|
||||
const LibraryItem* item = IndexToItem(index);
|
||||
|
||||
return data(item, role);
|
||||
}
|
||||
|
||||
QVariant LibraryModel::data(const LibraryItem* item, int role) const {
|
||||
GroupBy container_type =
|
||||
item->type == LibraryItem::Type_Container ?
|
||||
group_by_[item->container_level] : GroupBy_None;
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
case Qt::ToolTipRole:
|
||||
return item->DisplayText();
|
||||
|
||||
case Qt::DecorationRole:
|
||||
switch (item->type)
|
||||
case LibraryItem::Type_Container:
|
||||
switch (container_type) {
|
||||
case GroupBy_Album:
|
||||
case GroupBy_YearAlbum:
|
||||
return album_icon_;
|
||||
case GroupBy_Artist:
|
||||
return artist_icon_;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
break;
|
||||
|
||||
case Role_Type:
|
||||
return item->type;
|
||||
|
||||
case Role_ContainerType:
|
||||
return container_type;
|
||||
|
||||
case Role_Key:
|
||||
return item->key;
|
||||
|
||||
case Role_Artist:
|
||||
return item->metadata.artist();
|
||||
|
||||
case Role_SortText:
|
||||
if (item->type == LibraryItem::Type_Song)
|
||||
return item->metadata.disc() * 1000 + item->metadata.track();
|
||||
return item->SortText();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void LibraryModel::LazyPopulate(LibraryItem* parent, bool signal) {
|
||||
if (parent->lazy_loaded)
|
||||
return;
|
||||
parent->lazy_loaded = true;
|
||||
|
||||
// Information about what we want the children to be
|
||||
int child_level = parent->container_level + 1;
|
||||
GroupBy child_type = child_level >= 3 ? GroupBy_None : group_by_[child_level];
|
||||
|
||||
// Initialise the query. child_type says what type of thing we want (artists,
|
||||
// songs, etc.)
|
||||
LibraryQuery q(query_options_);
|
||||
InitQuery(child_type, &q);
|
||||
|
||||
// Top-level artists is special - we don't want compilation albums appearing
|
||||
if (child_level == 0 && child_type == GroupBy_Artist) {
|
||||
q.AddCompilationRequirement(false);
|
||||
}
|
||||
|
||||
// Walk up through the item's parents adding filters as necessary
|
||||
LibraryItem* p = parent;
|
||||
while (p != root_) {
|
||||
FilterQuery(group_by_[p->container_level], p, &q);
|
||||
p = p->parent;
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
if (!backend_->ExecQuery(&q))
|
||||
return;
|
||||
|
||||
// Step through the results
|
||||
while (q.Next()) {
|
||||
// Create the item - it will get inserted into the model here
|
||||
LibraryItem* item =
|
||||
ItemFromQuery(child_type, signal, child_level == 0, parent, q, child_level);
|
||||
|
||||
// Save a pointer to it for later
|
||||
if (child_type == GroupBy_None)
|
||||
song_nodes_[item->metadata.id()] = item;
|
||||
else
|
||||
container_nodes_[child_level][item->key] = item;
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryModel::Reset() {
|
||||
delete root_;
|
||||
song_nodes_.clear();
|
||||
container_nodes_[0].clear();
|
||||
container_nodes_[1].clear();
|
||||
container_nodes_[2].clear();
|
||||
divider_nodes_.clear();
|
||||
compilation_artist_node_ = NULL;
|
||||
|
||||
root_ = new LibraryItem(this);
|
||||
root_->lazy_loaded = false;
|
||||
|
||||
// Various artists?
|
||||
if (group_by_[0] == GroupBy_Artist &&
|
||||
backend_->HasCompilations(query_options_))
|
||||
CreateCompilationArtistNode(false, root_);
|
||||
|
||||
// Populate top level
|
||||
LazyPopulate(root_, false);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void LibraryModel::InitQuery(GroupBy type, LibraryQuery* q) {
|
||||
// Say what type of thing we want to get back from the database.
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
q->SetColumnSpec("DISTINCT artist");
|
||||
break;
|
||||
case GroupBy_Album:
|
||||
q->SetColumnSpec("DISTINCT album");
|
||||
break;
|
||||
case GroupBy_Composer:
|
||||
q->SetColumnSpec("DISTINCT composer");
|
||||
break;
|
||||
case GroupBy_YearAlbum:
|
||||
q->SetColumnSpec("DISTINCT year, album");
|
||||
break;
|
||||
case GroupBy_Year:
|
||||
q->SetColumnSpec("DISTINCT year");
|
||||
break;
|
||||
case GroupBy_Genre:
|
||||
q->SetColumnSpec("DISTINCT genre");
|
||||
break;
|
||||
case GroupBy_AlbumArtist:
|
||||
q->SetColumnSpec("DISTINCT albumartist");
|
||||
break;
|
||||
case GroupBy_None:
|
||||
q->SetColumnSpec("ROWID, " + Song::kColumnSpec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LibraryModel::FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q) {
|
||||
// Say how we want the query to be filtered. This is done once for each
|
||||
// parent going up the tree.
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
if (item == compilation_artist_node_)
|
||||
q->AddCompilationRequirement(true);
|
||||
else {
|
||||
if (item->container_level == 0) // Stupid hack
|
||||
q->AddCompilationRequirement(false);
|
||||
q->AddWhere("artist", item->key);
|
||||
}
|
||||
break;
|
||||
case GroupBy_Album:
|
||||
q->AddWhere("album", item->key);
|
||||
break;
|
||||
case GroupBy_YearAlbum:
|
||||
q->AddWhere("year", item->metadata.year());
|
||||
q->AddWhere("album", item->metadata.album());
|
||||
break;
|
||||
case GroupBy_Year:
|
||||
q->AddWhere("year", item->key);
|
||||
break;
|
||||
case GroupBy_Composer:
|
||||
q->AddWhere("composer", item->key);
|
||||
break;
|
||||
case GroupBy_Genre:
|
||||
q->AddWhere("genre", item->key);
|
||||
break;
|
||||
case GroupBy_AlbumArtist:
|
||||
q->AddWhere("albumartist", item->key);
|
||||
break;
|
||||
case GroupBy_None:
|
||||
Q_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LibraryItem* LibraryModel::InitItem(GroupBy type, bool signal, LibraryItem *parent,
|
||||
int container_level) {
|
||||
LibraryItem::Type item_type =
|
||||
type == GroupBy_None ? LibraryItem::Type_Song :
|
||||
LibraryItem::Type_Container;
|
||||
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent),
|
||||
parent->children.count(),parent->children.count());
|
||||
|
||||
// Initialise the item depending on what type it's meant to be
|
||||
LibraryItem* item = new LibraryItem(item_type, parent);
|
||||
item->container_level = container_level;
|
||||
return item;
|
||||
}
|
||||
|
||||
LibraryItem* LibraryModel::ItemFromQuery(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem* parent, const LibraryQuery& q,
|
||||
int container_level) {
|
||||
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
||||
int year = 0;
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
item->key = q.Value(0).toString();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortTextForArtist(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
year = qMax(0, q.Value(0).toInt());
|
||||
item->metadata.set_year(year);
|
||||
item->metadata.set_album(q.Value(1).toString());
|
||||
item->key = PrettyYearAlbum(year, item->metadata.album());
|
||||
item->sort_text = SortTextForYear(year) + item->metadata.album();
|
||||
break;
|
||||
|
||||
case GroupBy_Year:
|
||||
year = qMax(0, q.Value(0).toInt());
|
||||
item->key = QString::number(year);
|
||||
item->sort_text = SortTextForYear(year) + " ";
|
||||
break;
|
||||
|
||||
case GroupBy_Composer:
|
||||
case GroupBy_Genre:
|
||||
case GroupBy_Album:
|
||||
case GroupBy_AlbumArtist:
|
||||
item->key = q.Value(0).toString();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortText(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_None:
|
||||
item->metadata.InitFromQuery(q);
|
||||
item->key = item->metadata.title();
|
||||
item->display_text = item->metadata.PrettyTitleWithArtist();
|
||||
break;
|
||||
}
|
||||
|
||||
FinishItem(type, signal, create_divider, parent, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
LibraryItem* LibraryModel::ItemFromSong(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem* parent, const Song& s,
|
||||
int container_level) {
|
||||
LibraryItem* item = InitItem(type, signal, parent, container_level);
|
||||
int year = 0;
|
||||
|
||||
switch (type) {
|
||||
case GroupBy_Artist:
|
||||
item->key = s.artist();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortTextForArtist(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_YearAlbum:
|
||||
year = qMax(0, s.year());
|
||||
item->metadata.set_year(year);
|
||||
item->metadata.set_album(s.album());
|
||||
item->key = PrettyYearAlbum(year, s.album());
|
||||
item->sort_text = SortTextForYear(year) + s.album();
|
||||
break;
|
||||
|
||||
case GroupBy_Year:
|
||||
year = qMax(0, s.year());
|
||||
item->key = QString::number(year);
|
||||
item->sort_text = SortTextForYear(year) + " ";
|
||||
break;
|
||||
|
||||
case GroupBy_Composer: item->key = s.composer();
|
||||
case GroupBy_Genre: if (item->key.isNull()) item->key = s.genre();
|
||||
case GroupBy_Album: if (item->key.isNull()) item->key = s.album();
|
||||
case GroupBy_AlbumArtist: if (item->key.isNull()) item->key = s.albumartist();
|
||||
item->display_text = TextOrUnknown(item->key);
|
||||
item->sort_text = SortText(item->key);
|
||||
break;
|
||||
|
||||
case GroupBy_None:
|
||||
item->metadata = s;
|
||||
item->key = s.title();
|
||||
item->display_text = s.PrettyTitleWithArtist();
|
||||
break;
|
||||
}
|
||||
|
||||
FinishItem(type, signal, create_divider, parent, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
void LibraryModel::FinishItem(GroupBy type,
|
||||
bool signal, bool create_divider,
|
||||
LibraryItem *parent, LibraryItem *item) {
|
||||
if (type == GroupBy_None)
|
||||
item->lazy_loaded = true;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
|
||||
// Create the divider entry if we're supposed to
|
||||
if (create_divider) {
|
||||
QString divider_key = DividerKey(type, item);
|
||||
|
||||
if (!divider_key.isEmpty() && !divider_nodes_.contains(divider_key)) {
|
||||
if (signal)
|
||||
beginInsertRows(ItemToIndex(parent), parent->children.count(),
|
||||
parent->children.count());
|
||||
|
||||
LibraryItem* divider =
|
||||
new LibraryItem(LibraryItem::Type_Divider, root_);
|
||||
divider->key = divider_key;
|
||||
divider->display_text = DividerDisplayText(type, divider_key);
|
||||
divider->lazy_loaded = true;
|
||||
|
||||
divider_nodes_[divider_key] = divider;
|
||||
|
||||
if (signal)
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString LibraryModel::TextOrUnknown(const QString& text) const {
|
||||
if (text.isEmpty()) {
|
||||
return tr("Unknown");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
QString LibraryModel::PrettyYearAlbum(int year, const QString& album) const {
|
||||
if (year <= 0)
|
||||
return TextOrUnknown(album);
|
||||
return QString::number(year) + " - " + TextOrUnknown(album);
|
||||
}
|
||||
|
||||
QString LibraryModel::SortText(QString text) const {
|
||||
if (text.isEmpty()) {
|
||||
text = " unknown";
|
||||
} else {
|
||||
text = text.toLower();
|
||||
}
|
||||
text = text.remove(QRegExp("[^\\w ]"));
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
QString LibraryModel::SortTextForArtist(QString artist) const {
|
||||
artist = SortText(artist);
|
||||
|
||||
if (artist.startsWith("the ")) {
|
||||
artist = artist.right(artist.length() - 4) + ", the";
|
||||
}
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
QString LibraryModel::SortTextForYear(int year) const {
|
||||
QString str = QString::number(year);
|
||||
return QString("0").repeated(qMax(0, 4 - str.length())) + str;
|
||||
}
|
||||
|
||||
Qt::ItemFlags LibraryModel::flags(const QModelIndex& index) const {
|
||||
switch (IndexToItem(index)->type) {
|
||||
case LibraryItem::Type_Song:
|
||||
case LibraryItem::Type_Container:
|
||||
return Qt::ItemIsSelectable |
|
||||
Qt::ItemIsEnabled |
|
||||
Qt::ItemIsDragEnabled;
|
||||
case LibraryItem::Type_Divider:
|
||||
case LibraryItem::Type_Root:
|
||||
default:
|
||||
return Qt::ItemIsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
QStringList LibraryModel::mimeTypes() const {
|
||||
return QStringList() << "text/uri-list";
|
||||
}
|
||||
|
||||
QMimeData* LibraryModel::mimeData(const QModelIndexList& indexes) const {
|
||||
SongMimeData* data = new SongMimeData;
|
||||
QList<QUrl> urls;
|
||||
QSet<int> song_ids;
|
||||
|
||||
foreach (const QModelIndex& index, indexes) {
|
||||
GetChildSongs(IndexToItem(index), &urls, &data->songs, &song_ids);
|
||||
}
|
||||
|
||||
data->setUrls(urls);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
bool LibraryModel::CompareItems(const LibraryItem* a, const LibraryItem* b) const {
|
||||
QVariant left(data(a, LibraryModel::Role_SortText));
|
||||
QVariant right(data(b, LibraryModel::Role_SortText));
|
||||
|
||||
if (left.type() == QVariant::Int)
|
||||
return left.toInt() < right.toInt();
|
||||
return left.toString() < right.toString();
|
||||
}
|
||||
|
||||
void LibraryModel::GetChildSongs(LibraryItem* item, QList<QUrl>* urls,
|
||||
SongList* songs, QSet<int>* song_ids) const {
|
||||
switch (item->type) {
|
||||
case LibraryItem::Type_Container: {
|
||||
const_cast<LibraryModel*>(this)->LazyPopulate(item);
|
||||
|
||||
QList<LibraryItem*> children = item->children;
|
||||
qSort(children.begin(), children.end(), boost::bind(
|
||||
&LibraryModel::CompareItems, this, _1, _2));
|
||||
|
||||
foreach (LibraryItem* child, children)
|
||||
GetChildSongs(child, urls, songs, song_ids);
|
||||
break;
|
||||
}
|
||||
|
||||
case LibraryItem::Type_Song:
|
||||
urls->append(QUrl::fromLocalFile(item->metadata.filename()));
|
||||
if (!song_ids->contains(item->metadata.id())) {
|
||||
songs->append(item->metadata);
|
||||
song_ids->insert(item->metadata.id());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SongList LibraryModel::GetChildSongs(const QModelIndex& index) const {
|
||||
QList<QUrl> dontcare;
|
||||
SongList ret;
|
||||
QSet<int> song_ids;
|
||||
|
||||
if (!index.isValid())
|
||||
return SongList();
|
||||
|
||||
GetChildSongs(IndexToItem(index), &dontcare, &ret, &song_ids);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LibraryModel::SetFilterAge(int age) {
|
||||
query_options_.max_age = age;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void LibraryModel::SetFilterText(const QString& text) {
|
||||
query_options_.filter = text;
|
||||
Reset();
|
||||
}
|
||||
|
||||
bool LibraryModel::canFetchMore(const QModelIndex &parent) const {
|
||||
if (!parent.isValid())
|
||||
return false;
|
||||
|
||||
LibraryItem* item = IndexToItem(parent);
|
||||
return !item->lazy_loaded;
|
||||
}
|
||||
|
||||
void LibraryModel::SetGroupBy(const Grouping& g) {
|
||||
group_by_ = g;
|
||||
|
||||
Reset();
|
||||
emit GroupingChanged(g);
|
||||
}
|
||||
|
||||
const LibraryModel::GroupBy& LibraryModel::Grouping::operator [](int i) const {
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
case 1: return second;
|
||||
case 2: return third;
|
||||
}
|
||||
Q_ASSERT(0);
|
||||
return first;
|
||||
}
|
||||
|
||||
LibraryModel::GroupBy& LibraryModel::Grouping::operator [](int i) {
|
||||
switch (i) {
|
||||
case 0: return first;
|
||||
case 1: return second;
|
||||
case 2: return third;
|
||||
}
|
||||
Q_ASSERT(0);
|
||||
return first;
|
||||
}
|
188
src/librarymodel.h
Normal file
188
src/librarymodel.h
Normal file
@ -0,0 +1,188 @@
|
||||
/* 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 LIBRARYMODEL_H
|
||||
#define LIBRARYMODEL_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QIcon>
|
||||
|
||||
#include "backgroundthread.h"
|
||||
#include "librarywatcher.h"
|
||||
#include "libraryquery.h"
|
||||
#include "engines/engine_fwd.h"
|
||||
#include "song.h"
|
||||
#include "libraryitem.h"
|
||||
#include "simpletreemodel.h"
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
class LibraryDirectoryModel;
|
||||
class LibraryBackend;
|
||||
|
||||
class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
||||
Q_OBJECT
|
||||
Q_ENUMS(GroupBy);
|
||||
|
||||
public:
|
||||
LibraryModel(LibraryBackend* backend, QObject* parent = 0);
|
||||
~LibraryModel();
|
||||
|
||||
enum {
|
||||
Role_Type = Qt::UserRole + 1,
|
||||
Role_ContainerType,
|
||||
Role_SortText,
|
||||
Role_Key,
|
||||
Role_Artist,
|
||||
};
|
||||
|
||||
// These values get saved in QSettings - don't change them
|
||||
enum GroupBy {
|
||||
GroupBy_None = 0,
|
||||
GroupBy_Artist = 1,
|
||||
GroupBy_Album = 2,
|
||||
GroupBy_YearAlbum = 3,
|
||||
GroupBy_Year = 4,
|
||||
GroupBy_Composer = 5,
|
||||
GroupBy_Genre = 6,
|
||||
GroupBy_AlbumArtist = 7,
|
||||
};
|
||||
|
||||
struct Grouping {
|
||||
Grouping(GroupBy f = GroupBy_None,
|
||||
GroupBy s = GroupBy_None,
|
||||
GroupBy t = GroupBy_None)
|
||||
: first(f), second(s), third(t) {}
|
||||
|
||||
GroupBy first;
|
||||
GroupBy second;
|
||||
GroupBy third;
|
||||
|
||||
const GroupBy& operator [](int i) const;
|
||||
GroupBy& operator [](int i);
|
||||
bool operator ==(const Grouping& other) const {
|
||||
return first == other.first &&
|
||||
second == other.second &&
|
||||
third == other.third;
|
||||
}
|
||||
};
|
||||
|
||||
LibraryBackend* backend() const { return backend_; }
|
||||
LibraryDirectoryModel* directory_model() const { return dir_model_; }
|
||||
|
||||
// Get information about the library
|
||||
void GetChildSongs(LibraryItem* item, QList<QUrl>* urls, SongList* songs,
|
||||
QSet<int>* song_ids) const;
|
||||
SongList GetChildSongs(const QModelIndex& index) const;
|
||||
|
||||
// QAbstractItemModel
|
||||
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const;
|
||||
QStringList mimeTypes() const;
|
||||
QMimeData* mimeData(const QModelIndexList& indexes) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
|
||||
signals:
|
||||
void TotalSongCountUpdated(int count);
|
||||
void GroupingChanged(const LibraryModel::Grouping& g);
|
||||
|
||||
public slots:
|
||||
void SetFilterAge(int age);
|
||||
void SetFilterText(const QString& text);
|
||||
void SetGroupBy(const LibraryModel::Grouping& g);
|
||||
void Init();
|
||||
|
||||
protected:
|
||||
void LazyPopulate(LibraryItem* item) { LazyPopulate(item, false); }
|
||||
void LazyPopulate(LibraryItem* item, bool signal);
|
||||
|
||||
private slots:
|
||||
// From LibraryBackend
|
||||
void SongsDiscovered(const SongList& songs);
|
||||
void SongsDeleted(const SongList& songs);
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
void Initialise();
|
||||
|
||||
// Functions for working with queries and creating items.
|
||||
// When the model is reset or when a node is lazy-loaded the Library
|
||||
// constructs a database query to populate the items. Filters are added
|
||||
// for each parent item, restricting the songs returned to a particular
|
||||
// album or artist for example.
|
||||
void InitQuery(GroupBy type, LibraryQuery* q);
|
||||
void FilterQuery(GroupBy type, LibraryItem* item, LibraryQuery* q);
|
||||
|
||||
// Items can be created either from a query that's been run to populate a
|
||||
// node, or by a spontaneous SongsDiscovered emission from the backend.
|
||||
LibraryItem* ItemFromQuery(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, const LibraryQuery& q,
|
||||
int container_level);
|
||||
LibraryItem* ItemFromSong(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, const Song& s,
|
||||
int container_level);
|
||||
|
||||
// The "Various Artists" node is an annoying special case.
|
||||
LibraryItem* CreateCompilationArtistNode(bool signal, LibraryItem* parent);
|
||||
|
||||
// Helpers for ItemFromQuery and ItemFromSong
|
||||
LibraryItem* InitItem(GroupBy type, bool signal, LibraryItem* parent,
|
||||
int container_level);
|
||||
void FinishItem(GroupBy type, bool signal, bool create_divider,
|
||||
LibraryItem* parent, LibraryItem* item);
|
||||
|
||||
// Functions for manipulating text
|
||||
QString TextOrUnknown(const QString& text) const;
|
||||
QString PrettyYearAlbum(int year, const QString& album) const;
|
||||
|
||||
QString SortText(QString text) const;
|
||||
QString SortTextForArtist(QString artist) const;
|
||||
QString SortTextForYear(int year) const;
|
||||
|
||||
QString DividerKey(GroupBy type, LibraryItem* item) const;
|
||||
QString DividerDisplayText(GroupBy type, const QString& key) const;
|
||||
|
||||
// Helpers
|
||||
QVariant data(const LibraryItem* item, int role) const;
|
||||
bool CompareItems(const LibraryItem* a, const LibraryItem* b) const;
|
||||
|
||||
private:
|
||||
LibraryBackend* backend_;
|
||||
LibraryDirectoryModel* dir_model_;
|
||||
|
||||
QueryOptions query_options_;
|
||||
Grouping group_by_;
|
||||
|
||||
// Keyed on database ID
|
||||
QMap<int, LibraryItem*> song_nodes_;
|
||||
|
||||
// Keyed on whatever the key is for that level - artist, album, year, etc.
|
||||
QMap<QString, LibraryItem*> container_nodes_[3];
|
||||
|
||||
// Keyed on a letter, a year, a century, etc.
|
||||
QMap<QString, LibraryItem*> divider_nodes_;
|
||||
|
||||
// Only applies if the first level is "artist"
|
||||
LibraryItem* compilation_artist_node_;
|
||||
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
QIcon no_cover_icon_;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(LibraryModel::Grouping);
|
||||
|
||||
#endif // LIBRARYMODEL_H
|
@ -21,18 +21,13 @@
|
||||
#include <QDateTime>
|
||||
#include <QSqlError>
|
||||
|
||||
const char* QueryOptions::kLibraryTable = "songs";
|
||||
|
||||
QueryOptions::QueryOptions(const QString& _table)
|
||||
: table(_table),
|
||||
max_age(-1)
|
||||
QueryOptions::QueryOptions()
|
||||
: max_age(-1)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
LibraryQuery::LibraryQuery(const QueryOptions& options)
|
||||
: table_(options.table)
|
||||
{
|
||||
LibraryQuery::LibraryQuery(const QueryOptions& options) {
|
||||
if (!options.filter.isEmpty()) {
|
||||
where_clauses_ << "("
|
||||
"artist LIKE ? OR "
|
||||
@ -72,8 +67,8 @@ void LibraryQuery::AddCompilationRequirement(bool compilation) {
|
||||
where_clauses_ << QString("effective_compilation = %1").arg(compilation ? 1 : 0);
|
||||
}
|
||||
|
||||
QSqlError LibraryQuery::Exec(QSqlDatabase db) {
|
||||
QString sql = QString("SELECT %1 FROM %2").arg(column_spec_, table_);
|
||||
QSqlError LibraryQuery::Exec(QSqlDatabase db, const QString& table) {
|
||||
QString sql = QString("SELECT %1 FROM %2").arg(column_spec_, table);
|
||||
|
||||
if (!where_clauses_.isEmpty())
|
||||
sql += " WHERE " + where_clauses_.join(" AND ");
|
||||
|
@ -24,15 +24,13 @@
|
||||
#include <QVariantList>
|
||||
|
||||
class Song;
|
||||
class LibraryBackend;
|
||||
|
||||
struct QueryOptions {
|
||||
static const char* kLibraryTable;
|
||||
|
||||
QueryOptions(const QString& _table = kLibraryTable);
|
||||
QueryOptions();
|
||||
|
||||
bool Matches(const Song& song) const;
|
||||
|
||||
QString table;
|
||||
QString filter;
|
||||
int max_age;
|
||||
};
|
||||
@ -47,14 +45,13 @@ class LibraryQuery {
|
||||
void AddWhereLike(const QString& column, const QVariant& value);
|
||||
void AddCompilationRequirement(bool compilation);
|
||||
|
||||
QSqlError Exec(QSqlDatabase db);
|
||||
QSqlError Exec(QSqlDatabase db, const QString& table);
|
||||
bool Next();
|
||||
QVariant Value(int column) const;
|
||||
|
||||
operator const QSqlQuery& () const { return query_; }
|
||||
|
||||
private:
|
||||
QString table_;
|
||||
QString column_spec_;
|
||||
QString order_by_;
|
||||
QStringList where_clauses_;
|
||||
|
@ -14,9 +14,10 @@
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "library.h"
|
||||
#include "librarymodel.h"
|
||||
#include "libraryview.h"
|
||||
#include "libraryitem.h"
|
||||
#include "librarybackend.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QContextMenuEvent>
|
||||
@ -34,7 +35,7 @@ LibraryItemDelegate::LibraryItemDelegate(QObject *parent)
|
||||
|
||||
void LibraryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const {
|
||||
LibraryItem::Type type =
|
||||
static_cast<LibraryItem::Type>(index.data(Library::Role_Type).toInt());
|
||||
static_cast<LibraryItem::Type>(index.data(LibraryModel::Role_Type).toInt());
|
||||
|
||||
switch (type) {
|
||||
case LibraryItem::Type_Divider: {
|
||||
@ -103,7 +104,7 @@ void LibraryView::ReloadSettings() {
|
||||
auto_open_ = s.value("auto_open", true).toBool();
|
||||
}
|
||||
|
||||
void LibraryView::SetLibrary(Library *library) {
|
||||
void LibraryView::SetLibrary(LibraryModel *library) {
|
||||
library_ = library;
|
||||
}
|
||||
|
||||
@ -195,9 +196,9 @@ void LibraryView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
context_menu_index_ = qobject_cast<QSortFilterProxyModel*>(model())
|
||||
->mapToSource(context_menu_index_);
|
||||
|
||||
int type = library_->data(context_menu_index_, Library::Role_Type).toInt();
|
||||
int container_type = library_->data(context_menu_index_, Library::Role_ContainerType).toInt();
|
||||
bool enable_various = container_type == Library::GroupBy_Album;
|
||||
int type = library_->data(context_menu_index_, LibraryModel::Role_Type).toInt();
|
||||
int container_type = library_->data(context_menu_index_, LibraryModel::Role_ContainerType).toInt();
|
||||
bool enable_various = container_type == LibraryModel::GroupBy_Album;
|
||||
bool enable_add = type == LibraryItem::Type_Container ||
|
||||
type == LibraryItem::Type_Song;
|
||||
|
||||
@ -220,9 +221,9 @@ void LibraryView::ShowInVarious(bool on) {
|
||||
if (!context_menu_index_.isValid())
|
||||
return;
|
||||
|
||||
QString artist = library_->data(context_menu_index_, Library::Role_Artist).toString();
|
||||
QString album = library_->data(context_menu_index_, Library::Role_Key).toString();
|
||||
library_->GetBackend()->ForceCompilation(artist, album, on);
|
||||
QString artist = library_->data(context_menu_index_, LibraryModel::Role_Artist).toString();
|
||||
QString album = library_->data(context_menu_index_, LibraryModel::Role_Key).toString();
|
||||
library_->backend()->ForceCompilation(artist, album, on);
|
||||
}
|
||||
|
||||
void LibraryView::AddToPlaylist() {
|
||||
|
@ -20,7 +20,7 @@
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QTreeView>
|
||||
|
||||
class Library;
|
||||
class LibraryModel;
|
||||
|
||||
class LibraryItemDelegate : public QStyledItemDelegate {
|
||||
public:
|
||||
@ -36,7 +36,7 @@ class LibraryView : public QTreeView {
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
void SetLibrary(Library* library);
|
||||
void SetLibrary(LibraryModel* library);
|
||||
|
||||
public slots:
|
||||
void TotalSongCountUpdated(int count);
|
||||
@ -69,7 +69,7 @@ class LibraryView : public QTreeView {
|
||||
private:
|
||||
static const int kRowsToShow;
|
||||
|
||||
Library* library_;
|
||||
LibraryModel* library_;
|
||||
int total_song_count_;
|
||||
bool auto_open_;
|
||||
|
||||
|
@ -24,12 +24,10 @@
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
class QFileSystemWatcher;
|
||||
class QTimer;
|
||||
|
||||
class LibraryBackendInterface;
|
||||
class LibraryBackend;
|
||||
|
||||
class LibraryWatcher : public QObject {
|
||||
Q_OBJECT
|
||||
@ -37,7 +35,7 @@ class LibraryWatcher : public QObject {
|
||||
public:
|
||||
LibraryWatcher(QObject* parent = 0);
|
||||
|
||||
void SetBackend(boost::shared_ptr<LibraryBackendInterface> backend) { backend_ = backend; }
|
||||
void SetBackend(LibraryBackend* backend) { backend_ = backend; }
|
||||
|
||||
void Stop() { stop_requested_ = true; }
|
||||
|
||||
@ -120,7 +118,7 @@ class LibraryWatcher : public QObject {
|
||||
QFileSystemWatcher* watcher;
|
||||
};
|
||||
|
||||
boost::shared_ptr<LibraryBackendInterface> backend_;
|
||||
LibraryBackend* backend_;
|
||||
bool stop_requested_;
|
||||
|
||||
QMap<int, DirData> watched_dirs_;
|
||||
|
@ -45,6 +45,8 @@
|
||||
#include "commandlineoptions.h"
|
||||
#include "mac_startup.h"
|
||||
#include "transcodedialog.h"
|
||||
#include "playlistbackend.h"
|
||||
#include "database.h"
|
||||
|
||||
#include "globalshortcuts/globalshortcuts.h"
|
||||
|
||||
@ -90,14 +92,16 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
multi_loading_indicator_(new MultiLoadingIndicator(this)),
|
||||
library_config_dialog_(new LibraryConfigDialog),
|
||||
about_dialog_(new About),
|
||||
database_(new Database(this)),
|
||||
radio_model_(new RadioModel(this)),
|
||||
playlist_(new Playlist(this)),
|
||||
playlist_backend_(new PlaylistBackend(database_, this)),
|
||||
playlist_(new Playlist(playlist_backend_, this)),
|
||||
player_(new Player(playlist_, radio_model_->GetLastFMService(), engine, this)),
|
||||
library_(new Library(this)),
|
||||
library_(new Library(database_, this)),
|
||||
global_shortcuts_(new GlobalShortcuts(this)),
|
||||
settings_dialog_(new SettingsDialog),
|
||||
add_stream_dialog_(new AddStreamDialog),
|
||||
cover_manager_(new AlbumCoverManager(network)),
|
||||
cover_manager_(new AlbumCoverManager(network, library_->model()->backend())),
|
||||
group_by_dialog_(new GroupByDialog),
|
||||
equalizer_(new Equalizer),
|
||||
transcode_dialog_(new TranscodeDialog),
|
||||
@ -124,20 +128,20 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
#endif
|
||||
|
||||
// Models
|
||||
library_sort_model_->setSourceModel(library_);
|
||||
library_sort_model_->setSortRole(Library::Role_SortText);
|
||||
library_sort_model_->setSourceModel(library_->model());
|
||||
library_sort_model_->setSortRole(LibraryModel::Role_SortText);
|
||||
library_sort_model_->setDynamicSortFilter(true);
|
||||
library_sort_model_->sort(0);
|
||||
|
||||
playlist_->IgnoreSorting(true);
|
||||
ui_.playlist->setModel(playlist_);
|
||||
ui_.playlist->setItemDelegates(library_);
|
||||
ui_.playlist->SetItemDelegates(library_->model()->backend());
|
||||
playlist_->IgnoreSorting(false);
|
||||
|
||||
ui_.library_view->setModel(library_sort_model_);
|
||||
ui_.library_view->SetLibrary(library_);
|
||||
library_config_dialog_->SetModel(library_->GetDirectoryModel());
|
||||
settings_dialog_->SetLibraryDirectoryModel(library_->GetDirectoryModel());
|
||||
ui_.library_view->SetLibrary(library_->model());
|
||||
library_config_dialog_->SetModel(library_->model()->directory_model());
|
||||
settings_dialog_->SetLibraryDirectoryModel(library_->model()->directory_model());
|
||||
|
||||
ui_.radio_view->setModel(radio_model_);
|
||||
|
||||
@ -154,7 +158,7 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
connect(ui_.action_stop, SIGNAL(triggered()), player_, SLOT(Stop()));
|
||||
connect(ui_.action_quit, SIGNAL(triggered()), qApp, SLOT(quit()));
|
||||
connect(ui_.action_stop_after_this_track, SIGNAL(triggered()), SLOT(StopAfterCurrent()));
|
||||
connect(ui_.library_filter, SIGNAL(textChanged(QString)), library_, SLOT(SetFilterText(QString)));
|
||||
connect(ui_.library_filter, SIGNAL(textChanged(QString)), library_->model(), SLOT(SetFilterText(QString)));
|
||||
connect(ui_.action_ban, SIGNAL(triggered()), radio_model_->GetLastFMService(), SLOT(Ban()));
|
||||
connect(ui_.action_love, SIGNAL(triggered()), SLOT(Love()));
|
||||
connect(ui_.action_clear_playlist, SIGNAL(triggered()), playlist_, SLOT(Clear()));
|
||||
@ -225,18 +229,16 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
|
||||
connect(track_slider_, SIGNAL(ValueChanged(int)), player_, SLOT(Seek(int)));
|
||||
|
||||
// Database connections
|
||||
connect(database_, SIGNAL(Error(QString)), SLOT(ReportError(QString)));
|
||||
|
||||
// Library connections
|
||||
connect(library_, SIGNAL(Error(QString)), SLOT(ReportError(QString)));
|
||||
connect(ui_.library_view, SIGNAL(doubleClicked(QModelIndex)), SLOT(AddLibraryItemToPlaylist(QModelIndex)));
|
||||
connect(ui_.library_view, SIGNAL(AddToPlaylist(QModelIndex)), SLOT(AddLibraryItemToPlaylist(QModelIndex)));
|
||||
connect(ui_.library_view, SIGNAL(ShowConfigDialog()), library_config_dialog_.get(), SLOT(show()));
|
||||
connect(library_, SIGNAL(TotalSongCountUpdated(int)), ui_.library_view, SLOT(TotalSongCountUpdated(int)));
|
||||
connect(library_->model(), SIGNAL(TotalSongCountUpdated(int)), ui_.library_view, SLOT(TotalSongCountUpdated(int)));
|
||||
connect(library_, SIGNAL(ScanStarted()), SLOT(LibraryScanStarted()));
|
||||
connect(library_, SIGNAL(ScanFinished()), SLOT(LibraryScanFinished()));
|
||||
connect(library_, SIGNAL(BackendReady(boost::shared_ptr<LibraryBackendInterface>)),
|
||||
cover_manager_.get(), SLOT(SetBackend(boost::shared_ptr<LibraryBackendInterface>)));
|
||||
connect(library_, SIGNAL(BackendReady(boost::shared_ptr<LibraryBackendInterface>)),
|
||||
playlist_, SLOT(SetBackend(boost::shared_ptr<LibraryBackendInterface>)));
|
||||
|
||||
// Age filters
|
||||
QActionGroup* filter_age_group = new QActionGroup(this);
|
||||
@ -264,22 +266,22 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
connect(ui_.filter_age_month, SIGNAL(triggered()), filter_age_mapper, SLOT(map()));
|
||||
connect(ui_.filter_age_three_months, SIGNAL(triggered()), filter_age_mapper, SLOT(map()));
|
||||
connect(ui_.filter_age_year, SIGNAL(triggered()), filter_age_mapper, SLOT(map()));
|
||||
connect(filter_age_mapper, SIGNAL(mapped(int)), library_, SLOT(SetFilterAge(int)));
|
||||
connect(filter_age_mapper, SIGNAL(mapped(int)), library_->model(), SLOT(SetFilterAge(int)));
|
||||
connect(ui_.library_filter_clear, SIGNAL(clicked()), SLOT(ClearLibraryFilter()));
|
||||
|
||||
// "Group by ..."
|
||||
ui_.group_by_artist->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Artist)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Artist)));
|
||||
ui_.group_by_artist_album->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Artist, Library::GroupBy_Album)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Artist, LibraryModel::GroupBy_Album)));
|
||||
ui_.group_by_artist_yearalbum->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Artist, Library::GroupBy_YearAlbum)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Artist, LibraryModel::GroupBy_YearAlbum)));
|
||||
ui_.group_by_album->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Album)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Album)));
|
||||
ui_.group_by_genre_album->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Genre, Library::GroupBy_Album)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Genre, LibraryModel::GroupBy_Album)));
|
||||
ui_.group_by_genre_artist_album->setProperty("group_by", QVariant::fromValue(
|
||||
Library::Grouping(Library::GroupBy_Genre, Library::GroupBy_Artist, Library::GroupBy_Album)));
|
||||
LibraryModel::Grouping(LibraryModel::GroupBy_Genre, LibraryModel::GroupBy_Artist, LibraryModel::GroupBy_Album)));
|
||||
|
||||
group_by_group_ = new QActionGroup(this);
|
||||
group_by_group_->addAction(ui_.group_by_artist);
|
||||
@ -294,12 +296,12 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
group_by_menu->addActions(group_by_group_->actions());
|
||||
|
||||
connect(group_by_group_, SIGNAL(triggered(QAction*)), SLOT(GroupByClicked(QAction*)));
|
||||
connect(library_, SIGNAL(GroupingChanged(Library::Grouping)),
|
||||
group_by_dialog_.get(), SLOT(LibraryGroupingChanged(Library::Grouping)));
|
||||
connect(library_, SIGNAL(GroupingChanged(Library::Grouping)),
|
||||
SLOT(LibraryGroupingChanged(Library::Grouping)));
|
||||
connect(group_by_dialog_.get(), SIGNAL(Accepted(Library::Grouping)),
|
||||
library_, SLOT(SetGroupBy(Library::Grouping)));
|
||||
connect(library_->model(), SIGNAL(GroupingChanged(LibraryModel::Grouping)),
|
||||
group_by_dialog_.get(), SLOT(LibraryGroupingChanged(LibraryModel::Grouping)));
|
||||
connect(library_->model(), SIGNAL(GroupingChanged(LibraryModel::Grouping)),
|
||||
SLOT(LibraryGroupingChanged(LibraryModel::Grouping)));
|
||||
connect(group_by_dialog_.get(), SIGNAL(Accepted(LibraryModel::Grouping)),
|
||||
library_->model(), SLOT(SetGroupBy(LibraryModel::Grouping)));
|
||||
|
||||
// Library config menu
|
||||
QMenu* library_menu = new QMenu(this);
|
||||
@ -425,10 +427,10 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
|
||||
|
||||
ui_.file_view->SetPath(settings_.value("file_path", QDir::homePath()).toString());
|
||||
|
||||
library_->SetGroupBy(Library::Grouping(
|
||||
Library::GroupBy(settings_.value("group_by1", int(Library::GroupBy_Artist)).toInt()),
|
||||
Library::GroupBy(settings_.value("group_by2", int(Library::GroupBy_Album)).toInt()),
|
||||
Library::GroupBy(settings_.value("group_by3", int(Library::GroupBy_None)).toInt())));
|
||||
library_->model()->SetGroupBy(LibraryModel::Grouping(
|
||||
LibraryModel::GroupBy(settings_.value("group_by1", int(LibraryModel::GroupBy_Artist)).toInt()),
|
||||
LibraryModel::GroupBy(settings_.value("group_by2", int(LibraryModel::GroupBy_Album)).toInt()),
|
||||
LibraryModel::GroupBy(settings_.value("group_by3", int(LibraryModel::GroupBy_None)).toInt())));
|
||||
|
||||
#ifndef Q_OS_DARWIN
|
||||
StartupBehaviour behaviour =
|
||||
@ -579,7 +581,7 @@ void MainWindow::AddLibraryItemToPlaylist(const QModelIndex& index) {
|
||||
idx = library_sort_model_->mapToSource(idx);
|
||||
|
||||
QModelIndex first_song =
|
||||
playlist_->InsertLibraryItems(library_->GetChildSongs(idx));
|
||||
playlist_->InsertLibraryItems(library_->model()->GetChildSongs(idx));
|
||||
|
||||
if (first_song.isValid() && player_->GetState() != Engine::Playing)
|
||||
player_->PlayAt(first_song.row(), Engine::First, true);
|
||||
@ -795,7 +797,7 @@ void MainWindow::EditTracks() {
|
||||
}
|
||||
|
||||
edit_tag_dialog_->SetSongs(songs);
|
||||
edit_tag_dialog_->SetTagCompleter(library_);
|
||||
edit_tag_dialog_->SetTagCompleter(library_->model()->backend());
|
||||
|
||||
if (edit_tag_dialog_->exec() == QDialog::Rejected)
|
||||
return;
|
||||
@ -926,11 +928,11 @@ void MainWindow::GroupByClicked(QAction* action) {
|
||||
return;
|
||||
}
|
||||
|
||||
Library::Grouping g = action->property("group_by").value<Library::Grouping>();
|
||||
library_->SetGroupBy(g);
|
||||
LibraryModel::Grouping g = action->property("group_by").value<LibraryModel::Grouping>();
|
||||
library_->model()->SetGroupBy(g);
|
||||
}
|
||||
|
||||
void MainWindow::LibraryGroupingChanged(const Library::Grouping& g) {
|
||||
void MainWindow::LibraryGroupingChanged(const LibraryModel::Grouping& g) {
|
||||
// Save the settings
|
||||
settings_.setValue("group_by1", int(g[0]));
|
||||
settings_.setValue("group_by2", int(g[1]));
|
||||
@ -941,7 +943,7 @@ void MainWindow::LibraryGroupingChanged(const Library::Grouping& g) {
|
||||
if (action->property("group_by").isNull())
|
||||
continue;
|
||||
|
||||
if (g == action->property("group_by").value<Library::Grouping>()) {
|
||||
if (g == action->property("group_by").value<LibraryModel::Grouping>()) {
|
||||
action->setChecked(true);
|
||||
return;
|
||||
}
|
||||
|
@ -25,11 +25,13 @@
|
||||
|
||||
#include "ui_mainwindow.h"
|
||||
#include "engines/engine_fwd.h"
|
||||
#include "librarymodel.h"
|
||||
|
||||
class Playlist;
|
||||
class Player;
|
||||
class Library;
|
||||
class LibraryConfigDialog;
|
||||
class PlaylistBackend;
|
||||
class RadioModel;
|
||||
class Song;
|
||||
class RadioItem;
|
||||
@ -47,6 +49,7 @@ class GroupByDialog;
|
||||
class Equalizer;
|
||||
class CommandlineOptions;
|
||||
class TranscodeDialog;
|
||||
class Database;
|
||||
|
||||
class QSortFilterProxyModel;
|
||||
class SystemTrayIcon;
|
||||
@ -107,7 +110,7 @@ class MainWindow : public QMainWindow {
|
||||
void AddLibraryItemToPlaylist(const QModelIndex& index);
|
||||
void ClearLibraryFilter();
|
||||
void GroupByClicked(QAction*);
|
||||
void LibraryGroupingChanged(const Library::Grouping& g);
|
||||
void LibraryGroupingChanged(const LibraryModel::Grouping& g);
|
||||
|
||||
void VolumeWheelEvent(int delta);
|
||||
void TrayClicked(QSystemTrayIcon::ActivationReason reason);
|
||||
@ -149,7 +152,9 @@ class MainWindow : public QMainWindow {
|
||||
boost::scoped_ptr<LibraryConfigDialog> library_config_dialog_;
|
||||
boost::scoped_ptr<About> about_dialog_;
|
||||
|
||||
Database* database_;
|
||||
RadioModel* radio_model_;
|
||||
PlaylistBackend* playlist_backend_;
|
||||
Playlist* playlist_;
|
||||
Player* player_;
|
||||
Library* library_;
|
||||
|
@ -21,7 +21,7 @@
|
||||
#include "radioplaylistitem.h"
|
||||
#include "radiomodel.h"
|
||||
#include "savedradio.h"
|
||||
#include "librarybackend.h"
|
||||
#include "playlistbackend.h"
|
||||
#include "libraryplaylistitem.h"
|
||||
#include "playlistundocommands.h"
|
||||
|
||||
@ -43,9 +43,11 @@ using boost::shared_ptr;
|
||||
const char* Playlist::kRowsMimetype = "application/x-clementine-playlist-rows";
|
||||
const char* Playlist::kSettingsGroup = "Playlist";
|
||||
|
||||
Playlist::Playlist(QObject *parent, SettingsProvider* settings)
|
||||
Playlist::Playlist(PlaylistBackend* backend,
|
||||
QObject *parent, SettingsProvider* settings)
|
||||
: QAbstractListModel(parent),
|
||||
settings_(settings ? settings : new DefaultSettingsProvider),
|
||||
backend_(backend),
|
||||
current_is_paused_(false),
|
||||
current_virtual_index_(-1),
|
||||
is_shuffled_(false),
|
||||
@ -59,6 +61,8 @@ Playlist::Playlist(QObject *parent, SettingsProvider* settings)
|
||||
|
||||
connect(this, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SIGNAL(PlaylistChanged()));
|
||||
connect(this, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), SIGNAL(PlaylistChanged()));
|
||||
|
||||
Restore();
|
||||
}
|
||||
|
||||
Playlist::~Playlist() {
|
||||
@ -716,12 +720,6 @@ void Playlist::SetCurrentIsPaused(bool paused) {
|
||||
index(current_item_index_.row(), ColumnCount));
|
||||
}
|
||||
|
||||
void Playlist::SetBackend(shared_ptr<LibraryBackendInterface> backend) {
|
||||
backend_ = backend;
|
||||
|
||||
Restore();
|
||||
}
|
||||
|
||||
void Playlist::Save() const {
|
||||
if (!backend_)
|
||||
return;
|
||||
|
@ -29,7 +29,7 @@
|
||||
#include "settingsprovider.h"
|
||||
|
||||
class RadioService;
|
||||
class LibraryBackendInterface;
|
||||
class PlaylistBackend;
|
||||
|
||||
class QUndoStack;
|
||||
|
||||
@ -47,7 +47,8 @@ class Playlist : public QAbstractListModel {
|
||||
friend class PlaylistUndoCommands::MoveItems;
|
||||
|
||||
public:
|
||||
Playlist(QObject* parent = 0, SettingsProvider* settings = NULL);
|
||||
Playlist(PlaylistBackend* backend,
|
||||
QObject* parent = 0, SettingsProvider* settings = NULL);
|
||||
~Playlist();
|
||||
|
||||
enum Column {
|
||||
@ -145,8 +146,6 @@ class Playlist : public QAbstractListModel {
|
||||
|
||||
|
||||
public slots:
|
||||
void SetBackend(boost::shared_ptr<LibraryBackendInterface>);
|
||||
|
||||
void set_current_index(int index);
|
||||
void Paused();
|
||||
void Playing();
|
||||
@ -183,7 +182,7 @@ class Playlist : public QAbstractListModel {
|
||||
private:
|
||||
boost::scoped_ptr<SettingsProvider> settings_;
|
||||
|
||||
boost::shared_ptr<LibraryBackendInterface> backend_;
|
||||
PlaylistBackend* backend_;
|
||||
|
||||
PlaylistItemList items_;
|
||||
QList<int> virtual_items_; // Contains the indices into items_ in the order
|
||||
|
104
src/playlistbackend.cpp
Normal file
104
src/playlistbackend.cpp
Normal file
@ -0,0 +1,104 @@
|
||||
/* 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 "playlistbackend.h"
|
||||
#include "database.h"
|
||||
#include "scopedtransaction.h"
|
||||
#include "song.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QSqlQuery>
|
||||
|
||||
using boost::shared_ptr;
|
||||
|
||||
PlaylistBackend::PlaylistBackend(Database* db, QObject* parent)
|
||||
: QObject(parent),
|
||||
db_(db)
|
||||
{
|
||||
}
|
||||
|
||||
PlaylistBackend::PlaylistList PlaylistBackend::GetAllPlaylists() {
|
||||
qWarning() << "Not implemented:" << __PRETTY_FUNCTION__;
|
||||
return PlaylistList();
|
||||
}
|
||||
|
||||
PlaylistItemList PlaylistBackend::GetPlaylistItems(int playlist) {
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
PlaylistItemList ret;
|
||||
|
||||
QSqlQuery q("SELECT songs.ROWID, " + Song::kJoinSpec + ","
|
||||
" p.type, p.url, p.title, p.artist, p.album, p.length,"
|
||||
" p.radio_service"
|
||||
" FROM playlist_items AS p"
|
||||
" LEFT JOIN songs"
|
||||
" ON p.library_id = songs.ROWID"
|
||||
" WHERE p.playlist = :playlist", db);
|
||||
q.bindValue(":playlist", playlist);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q.lastError()))
|
||||
return ret;
|
||||
|
||||
while (q.next()) {
|
||||
// The song table gets joined first, plus one for the song ROWID
|
||||
const int row = Song::kColumns.count() + 1;
|
||||
|
||||
shared_ptr<PlaylistItem> item(
|
||||
PlaylistItem::NewFromType(q.value(row + 0).toString()));
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
if (item->InitFromQuery(q))
|
||||
ret << item;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items) {
|
||||
metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection,
|
||||
Q_ARG(int, playlist),
|
||||
Q_ARG(PlaylistItemList, items));
|
||||
}
|
||||
|
||||
void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList& items) {
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery clear("DELETE FROM playlist_items WHERE playlist = :playlist", db);
|
||||
QSqlQuery insert("INSERT INTO playlist_items"
|
||||
" (playlist, type, library_id, url, title, artist, album,"
|
||||
" length, radio_service)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", db);
|
||||
|
||||
ScopedTransaction transaction(&db);
|
||||
|
||||
// Clear the existing items in the playlist
|
||||
clear.bindValue(":playlist", playlist);
|
||||
clear.exec();
|
||||
if (db_->CheckErrors(clear.lastError()))
|
||||
return;
|
||||
|
||||
// Save the new ones
|
||||
foreach (shared_ptr<PlaylistItem> item, items) {
|
||||
insert.bindValue(0, playlist);
|
||||
item->BindToQuery(&insert);
|
||||
|
||||
insert.exec();
|
||||
db_->CheckErrors(insert.lastError());
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
50
src/playlistbackend.h
Normal file
50
src/playlistbackend.h
Normal file
@ -0,0 +1,50 @@
|
||||
/* 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 PLAYLISTBACKEND_H
|
||||
#define PLAYLISTBACKEND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
|
||||
#include "playlistitem.h"
|
||||
|
||||
class Database;
|
||||
|
||||
class PlaylistBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PlaylistBackend(Database* db, QObject* parent = 0);
|
||||
|
||||
struct Playlist {
|
||||
int id;
|
||||
QString name;
|
||||
};
|
||||
typedef QList<Playlist> PlaylistList;
|
||||
|
||||
PlaylistList GetAllPlaylists();
|
||||
PlaylistItemList GetPlaylistItems(int playlist);
|
||||
void SavePlaylistAsync(int playlist, const PlaylistItemList& items);
|
||||
|
||||
public slots:
|
||||
void SavePlaylist(int playlist, const PlaylistItemList& items);
|
||||
|
||||
private:
|
||||
Database* db_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTBACKEND_H
|
@ -16,6 +16,7 @@
|
||||
|
||||
#include "playlistdelegates.h"
|
||||
#include "trackslider.h"
|
||||
#include "librarybackend.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QLineEdit>
|
||||
@ -212,17 +213,17 @@ QWidget* TextItemDelegate::createEditor(
|
||||
}
|
||||
|
||||
|
||||
TagCompletionModel::TagCompletionModel(Library* library, Playlist::Column column) :
|
||||
TagCompletionModel::TagCompletionModel(LibraryBackend* backend, Playlist::Column column) :
|
||||
QStringListModel() {
|
||||
|
||||
switch(column) {
|
||||
case Playlist::Column_Artist: {
|
||||
setStringList(library->GetBackend()->GetAllArtists());
|
||||
setStringList(backend->GetAllArtists());
|
||||
break;
|
||||
}
|
||||
case Playlist::Column_Album: {
|
||||
QStringList album_names;
|
||||
LibraryBackend::AlbumList albums = library->GetBackend()->GetAllAlbums();
|
||||
LibraryBackend::AlbumList albums = backend->GetAllAlbums();
|
||||
foreach(const LibraryBackend::Album& album, albums)
|
||||
album_names << album.album_name;
|
||||
setStringList(album_names);
|
||||
@ -237,19 +238,19 @@ TagCompletionModel::TagCompletionModel(Library* library, Playlist::Column column
|
||||
}
|
||||
}
|
||||
|
||||
TagCompleter::TagCompleter(Library* library, Playlist::Column column, QLineEdit* editor) :
|
||||
TagCompleter::TagCompleter(LibraryBackend* backend, Playlist::Column column, QLineEdit* editor) :
|
||||
QCompleter(editor) {
|
||||
|
||||
setModel(new TagCompletionModel(library, column));
|
||||
setModel(new TagCompletionModel(backend, column));
|
||||
setCaseSensitivity(Qt::CaseInsensitive);
|
||||
editor->setCompleter(this);
|
||||
}
|
||||
|
||||
QWidget* TagCompletionItemDelegate::createEditor(
|
||||
QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const {
|
||||
QWidget* parent, const QStyleOptionViewItem&, const QModelIndex&) const {
|
||||
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
new TagCompleter(library_, column_, editor);
|
||||
new TagCompleter(backend_, column_, editor);
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
@ -78,24 +78,24 @@ class TextItemDelegate : public PlaylistDelegateBase {
|
||||
|
||||
class TagCompletionModel : public QStringListModel {
|
||||
public:
|
||||
TagCompletionModel(Library* library, Playlist::Column column);
|
||||
TagCompletionModel(LibraryBackend* backend, Playlist::Column column);
|
||||
};
|
||||
|
||||
class TagCompleter : public QCompleter {
|
||||
public:
|
||||
TagCompleter(Library* library, Playlist::Column column, QLineEdit* editor);
|
||||
TagCompleter(LibraryBackend* backend, Playlist::Column column, QLineEdit* editor);
|
||||
};
|
||||
|
||||
class TagCompletionItemDelegate : public PlaylistDelegateBase {
|
||||
public:
|
||||
TagCompletionItemDelegate(QTreeView* view, Library* library, Playlist::Column column) :
|
||||
PlaylistDelegateBase(view), library_(library), column_(column) {};
|
||||
TagCompletionItemDelegate(QTreeView* view,LibraryBackend* backend, Playlist::Column column) :
|
||||
PlaylistDelegateBase(view), backend_(backend), column_(column) {};
|
||||
|
||||
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const;
|
||||
|
||||
private:
|
||||
Library* library_;
|
||||
LibraryBackend* backend_;
|
||||
Playlist::Column column_;
|
||||
};
|
||||
|
||||
|
@ -61,15 +61,15 @@ PlaylistView::PlaylistView(QWidget *parent)
|
||||
setAlternatingRowColors(true);
|
||||
}
|
||||
|
||||
void PlaylistView::setItemDelegates(Library* library) {
|
||||
void PlaylistView::SetItemDelegates(LibraryBackend* backend) {
|
||||
setItemDelegate(new PlaylistDelegateBase(this));
|
||||
setItemDelegateForColumn(Playlist::Column_Title, new TextItemDelegate(this));
|
||||
setItemDelegateForColumn(Playlist::Column_Album,
|
||||
new TagCompletionItemDelegate(this, library, Playlist::Column_Album));
|
||||
new TagCompletionItemDelegate(this, backend, Playlist::Column_Album));
|
||||
setItemDelegateForColumn(Playlist::Column_Artist,
|
||||
new TagCompletionItemDelegate(this, library, Playlist::Column_Artist));
|
||||
new TagCompletionItemDelegate(this, backend, Playlist::Column_Artist));
|
||||
setItemDelegateForColumn(Playlist::Column_AlbumArtist,
|
||||
new TagCompletionItemDelegate(this, library, Playlist::Column_AlbumArtist));
|
||||
new TagCompletionItemDelegate(this, backend, Playlist::Column_AlbumArtist));
|
||||
setItemDelegateForColumn(Playlist::Column_Length, new LengthItemDelegate(this));
|
||||
setItemDelegateForColumn(Playlist::Column_Filesize, new SizeItemDelegate(this));
|
||||
setItemDelegateForColumn(Playlist::Column_Filetype, new FileTypeItemDelegate(this));
|
||||
|
@ -18,12 +18,12 @@
|
||||
#define PLAYLISTVIEW_H
|
||||
|
||||
#include "playlist.h"
|
||||
#include "library.h"
|
||||
|
||||
#include <QTreeView>
|
||||
#include <QBasicTimer>
|
||||
|
||||
class RadioLoadingIndicator;
|
||||
class LibraryBackend;
|
||||
|
||||
class PlaylistView : public QTreeView {
|
||||
Q_OBJECT
|
||||
@ -31,7 +31,7 @@ class PlaylistView : public QTreeView {
|
||||
public:
|
||||
PlaylistView(QWidget* parent = 0);
|
||||
|
||||
void setItemDelegates(Library* library);
|
||||
void SetItemDelegates(LibraryBackend* backend);
|
||||
void RemoveSelected();
|
||||
|
||||
// QTreeView
|
||||
|
@ -53,12 +53,6 @@ msgstr "Upravit tag\"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Různí umělci"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Neznámý"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titulek"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Všechny soubory (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Neznámý"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Různí umělci"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Redigér mærke \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse kunstnere"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Ukendt"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
@ -400,6 +394,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Alle filer (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Ukendt"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -629,6 +626,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse kunstnere"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "%1 bearbeiten"
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Verschiedene Interpreten"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Unbekannt"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
@ -398,6 +392,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Alle Dateien (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Unbekannt"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -625,6 +622,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Verschiedene Interpreten"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -54,12 +54,6 @@ msgstr "Τροποποίηση ετικέτας \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Διάφοροι καλλιτέχνες"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Άγνωστο"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Τίτλος"
|
||||
|
||||
@ -400,6 +394,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Όλα τα αρχεία (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Άγνωστο"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -627,6 +624,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Διάφοροι καλλιτέχνες"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "Edit tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Various Artists"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Unknown"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Title"
|
||||
|
||||
@ -398,6 +392,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "All files (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Unknown"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -625,6 +622,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Various Artists"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Editar etiqueta \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Varios Artistas"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconocido"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@ -400,6 +394,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Todos los archivos (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconocido"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -627,6 +624,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Varios Artistas"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Useita artisteja"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Tuntematon"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Kappale"
|
||||
|
||||
@ -397,6 +391,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Tuntematon"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -624,6 +621,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Useita artisteja"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Modifer le tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Compilations d'artistes"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Inconnu"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titre"
|
||||
|
||||
@ -401,6 +395,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Tous les fichiers (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Inconnu"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -628,6 +625,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Compilations d'artistes"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "Editar a tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários Artistas"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Descoñecido"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Todos os ficheiros (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Descoñecido"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários Artistas"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Modifica tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr "Aggiungi media"
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Artisti vari"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Sconosciuto"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titolo"
|
||||
|
||||
@ -400,6 +394,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Tutti i file (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Sconosciuto"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -627,6 +624,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr "Aggiungi file da convertire"
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Artisti vari"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Белгісіз"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Аталуы"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Белгісіз"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "Endre merkelapp \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse artister"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Ukjent"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Tittel"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Alle filer (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Ukjent"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse artister"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Edytuj znacznik \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Różni wykonawcy"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "nieznany"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Nazwa"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Wszystkie pliki (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "nieznany"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Różni wykonawcy"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "Editar a tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários Artistas"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconhecido"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@ -400,6 +394,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Todos os ficheiros (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconhecido"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -627,6 +624,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários Artistas"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários artistas"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconhecido"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
@ -397,6 +391,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Desconhecido"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -624,6 +621,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Vários artistas"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diferiți artiști"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Necunoscut"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titlu"
|
||||
|
||||
@ -398,6 +392,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Toate fișierele (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Necunoscut"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -625,6 +622,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diferiți artiști"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -51,12 +51,6 @@ msgstr "Редактировать тег \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr "Добавить файлы"
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Разные исполнители"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Неизвестный"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Название"
|
||||
|
||||
@ -398,6 +392,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Все файлы (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Неизвестный"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -625,6 +622,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr "Добавить файлы для перекодирования"
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Разные исполнители"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -53,12 +53,6 @@ msgstr "Upraviť tag \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Rôzni interpréti"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "neznámy"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Názov"
|
||||
|
||||
@ -399,6 +393,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Všetky súbory (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "neznámy"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -626,6 +623,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Rôzni interpréti"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr "Redigera tagg \"%1\"..."
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse artister"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Okänt"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
@ -398,6 +392,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr "Alla filer (*)"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Okänt"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr "ASF"
|
||||
|
||||
@ -625,6 +622,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr "Diverse artister"
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr "Clementine"
|
||||
|
||||
|
@ -52,12 +52,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Bilinmiyor"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Başlık"
|
||||
|
||||
@ -397,6 +391,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Bilinmiyor"
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -624,6 +621,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43,12 +43,6 @@ msgstr ""
|
||||
msgid "Add media"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
@ -388,6 +382,9 @@ msgstr ""
|
||||
msgid "All files (*)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
msgid "ASF"
|
||||
msgstr ""
|
||||
|
||||
@ -615,6 +612,9 @@ msgstr ""
|
||||
msgid "Add files to transcode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Various Artists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clementine"
|
||||
msgstr ""
|
||||
|
||||
|
@ -91,10 +91,11 @@ endmacro (add_test_file)
|
||||
|
||||
add_test_file(m3uparser_test.cpp false)
|
||||
add_test_file(song_test.cpp false)
|
||||
add_test_file(database_test.cpp false)
|
||||
add_test_file(librarybackend_test.cpp false)
|
||||
add_test_file(albumcoverfetcher_test.cpp false)
|
||||
add_test_file(xspfparser_test.cpp false)
|
||||
add_test_file(library_test.cpp false)
|
||||
add_test_file(librarymodel_test.cpp false)
|
||||
add_test_file(albumcovermanager_test.cpp true)
|
||||
add_test_file(songplaylistitem_test.cpp false)
|
||||
add_test_file(translations_test.cpp false)
|
||||
|
@ -23,7 +23,7 @@
|
||||
class AlbumCoverManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
AlbumCoverManagerTest()
|
||||
: manager_(&network_) {
|
||||
: manager_(&network_, NULL) {
|
||||
}
|
||||
|
||||
MockNetworkAccessManager network_;
|
||||
|
126
tests/database_test.cpp
Normal file
126
tests/database_test.cpp
Normal file
@ -0,0 +1,126 @@
|
||||
/* 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 "gtest/gtest.h"
|
||||
|
||||
#include "database.h"
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QSqlQuery>
|
||||
#include <QVariant>
|
||||
|
||||
class DatabaseTest : public ::testing::Test {
|
||||
protected:
|
||||
virtual void SetUp() {
|
||||
database_.reset(new MemoryDatabase);
|
||||
}
|
||||
|
||||
boost::scoped_ptr<Database> database_;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
struct PerfTimer {
|
||||
PerfTimer(int iterations) : iterations_(iterations) {
|
||||
gettimeofday(&start_time_, NULL);
|
||||
}
|
||||
|
||||
~PerfTimer() {
|
||||
gettimeofday(&end_time_, NULL);
|
||||
|
||||
timeval elapsed_time;
|
||||
timersub(&end_time_, &start_time_, &elapsed_time);
|
||||
int elapsed_us = elapsed_time.tv_usec + elapsed_time.tv_sec * 1000000;
|
||||
|
||||
qDebug() << "Elapsed:" << elapsed_us << "us";
|
||||
qDebug() << "Time per iteration:" << float(elapsed_us) / iterations_ << "us";
|
||||
}
|
||||
|
||||
timeval start_time_;
|
||||
timeval end_time_;
|
||||
int iterations_;
|
||||
};
|
||||
|
||||
TEST_F(DatabaseTest, LikePerformance) {
|
||||
const int iterations = 1000000;
|
||||
|
||||
const char* needle = "foo";
|
||||
const char* haystack = "foobarbaz foobarbaz";
|
||||
qDebug() << "Simple query";
|
||||
{
|
||||
PerfTimer perf(iterations);
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
database_->Like(needle, haystack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_F(DatabaseTest, DatabaseInitialises) {
|
||||
// Check that these tables exist
|
||||
QStringList tables = database_->Connect().tables();
|
||||
EXPECT_TRUE(tables.contains("songs"));
|
||||
EXPECT_TRUE(tables.contains("directories"));
|
||||
EXPECT_TRUE(tables.contains("subdirectories"));
|
||||
EXPECT_TRUE(tables.contains("playlists"));
|
||||
EXPECT_TRUE(tables.contains("playlist_items"));
|
||||
ASSERT_TRUE(tables.contains("schema_version"));
|
||||
|
||||
// Check the schema version is correct
|
||||
QSqlQuery q("SELECT version FROM schema_version", database_->Connect());
|
||||
ASSERT_TRUE(q.exec());
|
||||
ASSERT_TRUE(q.next());
|
||||
EXPECT_EQ(Database::kSchemaVersion, q.value(0).toInt());
|
||||
EXPECT_FALSE(q.next());
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeWorksWithAllAscii) {
|
||||
EXPECT_TRUE(database_->Like("%ar%", "bar"));
|
||||
EXPECT_FALSE(database_->Like("%ar%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeWorksWithUnicode) {
|
||||
EXPECT_TRUE(database_->Like("%Снег%", "Снег"));
|
||||
EXPECT_FALSE(database_->Like("%Снег%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeAsciiCaseInsensitive) {
|
||||
EXPECT_TRUE(database_->Like("%ar%", "BAR"));
|
||||
EXPECT_FALSE(database_->Like("%ar%", "FOO"));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeUnicodeCaseInsensitive) {
|
||||
EXPECT_TRUE(database_->Like("%снег%", "Снег"));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeCacheInvalidated) {
|
||||
EXPECT_TRUE(database_->Like("%foo%", "foobar"));
|
||||
EXPECT_FALSE(database_->Like("%baz%", "foobar"));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, LikeQuerySplit) {
|
||||
EXPECT_TRUE(database_->Like("%foo bar%", "foobar"));
|
||||
EXPECT_FALSE(database_->Like("%foo bar%", "barbaz"));
|
||||
EXPECT_FALSE(database_->Like("%foo bar%", "foobaz"));
|
||||
EXPECT_FALSE(database_->Like("%foo bar%", "baz"));
|
||||
}
|
@ -1,310 +0,0 @@
|
||||
/* 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 "gtest/gtest.h"
|
||||
|
||||
#include "library.h"
|
||||
#include "backgroundthread.h"
|
||||
#include "mock_backgroundthread.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QSignalSpy>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
namespace {
|
||||
|
||||
class LibraryTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() {
|
||||
library_ = new Library(
|
||||
static_cast<EngineBase*>(NULL), static_cast<QObject*>(NULL));
|
||||
library_->set_backend_factory(
|
||||
new FakeBackgroundThreadFactory<LibraryBackendInterface, MemoryLibraryBackend>);
|
||||
library_->set_watcher_factory(
|
||||
new FakeBackgroundThreadFactory<LibraryWatcher, LibraryWatcher>);
|
||||
|
||||
library_->Init();
|
||||
|
||||
added_dir_ = false;
|
||||
backend_ = library_->GetBackend().get();
|
||||
connection_name_ = "thread_" + QString::number(
|
||||
reinterpret_cast<quint64>(QThread::currentThread()));
|
||||
database_ = QSqlDatabase::database(connection_name_);
|
||||
|
||||
library_sorted_ = new QSortFilterProxyModel;
|
||||
library_sorted_->setSourceModel(library_);
|
||||
library_sorted_->setSortRole(Library::Role_SortText);
|
||||
library_sorted_->setDynamicSortFilter(true);
|
||||
library_sorted_->sort(0);
|
||||
}
|
||||
|
||||
void TearDown() {
|
||||
// Make sure Qt does not re-use the connection.
|
||||
database_ = QSqlDatabase();
|
||||
QSqlDatabase::removeDatabase(connection_name_);
|
||||
|
||||
delete library_;
|
||||
delete library_sorted_;
|
||||
}
|
||||
|
||||
Song AddSong(Song& song) {
|
||||
song.set_directory_id(1);
|
||||
if (song.mtime() == -1) song.set_mtime(0);
|
||||
if (song.ctime() == -1) song.set_ctime(0);
|
||||
if (song.filename().isNull()) song.set_filename("/test/foo");
|
||||
if (song.filesize() == -1) song.set_filesize(0);
|
||||
|
||||
if (!added_dir_) {
|
||||
backend_->AddDirectory("/test");
|
||||
added_dir_ = true;
|
||||
}
|
||||
|
||||
backend_->AddOrUpdateSongs(SongList() << song);
|
||||
return song;
|
||||
}
|
||||
|
||||
Song AddSong(const QString& title, const QString& artist, const QString& album, int length) {
|
||||
Song song;
|
||||
song.Init(title, artist, album, length);
|
||||
return AddSong(song);
|
||||
}
|
||||
|
||||
Library* library_;
|
||||
LibraryBackendInterface* backend_;
|
||||
QSortFilterProxyModel* library_sorted_;
|
||||
|
||||
bool added_dir_;
|
||||
QString connection_name_;
|
||||
QSqlDatabase database_;
|
||||
};
|
||||
|
||||
TEST_F(LibraryTest, Initialisation) {
|
||||
library_->StartThreads();
|
||||
|
||||
EXPECT_EQ(0, library_->rowCount(QModelIndex()));
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, WithInitialArtists) {
|
||||
AddSong("Title", "Artist 1", "Album", 123);
|
||||
AddSong("Title", "Artist 2", "Album", 123);
|
||||
AddSong("Title", "Foo", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
ASSERT_EQ(5, library_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("A", library_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist 1", library_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist 2", library_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("F", library_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Foo", library_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, CompilationAlbums) {
|
||||
Song song;
|
||||
song.Init("Title", "Artist", "Album", 123);
|
||||
song.set_compilation(true);
|
||||
|
||||
AddSong(song);
|
||||
library_->StartThreads();
|
||||
|
||||
ASSERT_EQ(1, library_->rowCount(QModelIndex()));
|
||||
|
||||
QModelIndex va_index = library_->index(0, 0, QModelIndex());
|
||||
EXPECT_EQ("Various Artists", va_index.data().toString());
|
||||
EXPECT_TRUE(library_->hasChildren(va_index));
|
||||
ASSERT_EQ(library_->rowCount(va_index), 1);
|
||||
|
||||
QModelIndex album_index = library_->index(0, 0, va_index);
|
||||
EXPECT_EQ(library_->data(album_index).toString(), "Album");
|
||||
EXPECT_TRUE(library_->hasChildren(album_index));
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, NumericHeaders) {
|
||||
AddSong("Title", "1artist", "Album", 123);
|
||||
AddSong("Title", "2artist", "Album", 123);
|
||||
AddSong("Title", "0artist", "Album", 123);
|
||||
AddSong("Title", "zartist", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
ASSERT_EQ(6, library_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("0-9", library_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("0artist", library_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("1artist", library_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("2artist", library_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Z", library_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("zartist", library_sorted_->index(5, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, MixedCaseHeaders) {
|
||||
AddSong("Title", "Artist", "Album", 123);
|
||||
AddSong("Title", "artist", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
ASSERT_EQ(3, library_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("A", library_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist", library_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("artist", library_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, UnknownArtists) {
|
||||
AddSong("Title", "", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
ASSERT_EQ(1, library_->rowCount(QModelIndex()));
|
||||
QModelIndex unknown_index = library_->index(0, 0, QModelIndex());
|
||||
EXPECT_EQ("Unknown", unknown_index.data().toString());
|
||||
|
||||
ASSERT_EQ(1, library_->rowCount(unknown_index));
|
||||
EXPECT_EQ("Album", library_->index(0, 0, unknown_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, UnknownAlbums) {
|
||||
AddSong("Title", "Artist", "", 123);
|
||||
AddSong("Title", "Artist", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
QModelIndex artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(2, library_->rowCount(artist_index));
|
||||
|
||||
QModelIndex unknown_album_index = library_->index(0, 0, artist_index);
|
||||
QModelIndex real_album_index = library_->index(1, 0, artist_index);
|
||||
|
||||
EXPECT_EQ("Unknown", unknown_album_index.data().toString());
|
||||
EXPECT_EQ("Album", real_album_index.data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, VariousArtistSongs) {
|
||||
SongList songs;
|
||||
for (int i=0 ; i<4 ; ++i) {
|
||||
QString n = QString::number(i+1);
|
||||
Song song;
|
||||
song.Init("Title " + n, "Artist " + n, "Album", 0);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
// Different ways of putting songs in "Various Artist". Make sure they all work
|
||||
songs[0].set_sampler(true);
|
||||
songs[1].set_compilation(true);
|
||||
songs[2].set_forced_compilation_on(true);
|
||||
songs[3].set_sampler(true); songs[3].set_artist("Various Artists");
|
||||
|
||||
for (int i=0 ; i<4 ; ++i)
|
||||
AddSong(songs[i]);
|
||||
library_->StartThreads();
|
||||
|
||||
QModelIndex artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, library_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = library_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(4, library_->rowCount(album_index));
|
||||
|
||||
EXPECT_EQ("Artist 1 - Title 1", library_->index(0, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Artist 2 - Title 2", library_->index(1, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Artist 3 - Title 3", library_->index(2, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Title 4", library_->index(3, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, RemoveSongsLazyLoaded) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album", 123); two.set_id(2);
|
||||
AddSong("Title 3", "Artist", "Album", 123);
|
||||
library_->StartThreads();
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, library_->rowCount(artist_index));
|
||||
QModelIndex album_index = library_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(3, library_->rowCount(album_index));
|
||||
|
||||
// Remove the first two songs
|
||||
QSignalSpy spy_preremove(library_, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_remove(library_, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_reset(library_, SIGNAL(modelReset()));
|
||||
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
ASSERT_EQ(2, spy_preremove.count());
|
||||
ASSERT_EQ(2, spy_remove.count());
|
||||
ASSERT_EQ(0, spy_reset.count());
|
||||
|
||||
artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, library_->rowCount(artist_index));
|
||||
album_index = library_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(1, library_->rowCount(album_index));
|
||||
EXPECT_EQ("Title 3", library_->index(0, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, RemoveSongsNotLazyLoaded) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album", 123); two.set_id(2);
|
||||
library_->StartThreads();
|
||||
|
||||
// Remove the first two songs
|
||||
QSignalSpy spy_preremove(library_, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_remove(library_, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_reset(library_, SIGNAL(modelReset()));
|
||||
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
ASSERT_EQ(0, spy_preremove.count());
|
||||
ASSERT_EQ(0, spy_remove.count());
|
||||
ASSERT_EQ(1, spy_reset.count());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, RemoveEmptyAlbums) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album 1", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album 2", 123); two.set_id(2);
|
||||
Song three = AddSong("Title 3", "Artist", "Album 2", 123); three.set_id(3);
|
||||
library_->StartThreads();
|
||||
|
||||
QModelIndex artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(2, library_->rowCount(artist_index));
|
||||
|
||||
// Remove one song from each album
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
// Check the model
|
||||
artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, library_->rowCount(artist_index));
|
||||
QModelIndex album_index = library_->index(0, 0, artist_index);
|
||||
EXPECT_EQ("Album 2", album_index.data().toString());
|
||||
|
||||
ASSERT_EQ(1, library_->rowCount(album_index));
|
||||
EXPECT_EQ("Title 3", library_->index(0, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryTest, RemoveEmptyArtists) {
|
||||
Song one = AddSong("Title", "Artist", "Album", 123); one.set_id(1);
|
||||
library_->StartThreads();
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = library_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, library_->rowCount(artist_index));
|
||||
QModelIndex album_index = library_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(1, library_->rowCount(album_index));
|
||||
|
||||
// The artist header is there too right?
|
||||
ASSERT_EQ(2, library_->rowCount(QModelIndex()));
|
||||
|
||||
// Remove the song
|
||||
backend_->DeleteSongs(SongList() << one);
|
||||
|
||||
// Everything should be gone - even the artist header
|
||||
ASSERT_EQ(0, library_->rowCount(QModelIndex()));
|
||||
}
|
||||
|
||||
} // namespace
|
@ -16,10 +16,11 @@
|
||||
|
||||
#include "test_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
#include "librarybackend.h"
|
||||
#include "library.h"
|
||||
#include "song.h"
|
||||
#include "database.h"
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
@ -27,25 +28,14 @@
|
||||
#include <QThread>
|
||||
#include <QSignalSpy>
|
||||
|
||||
using ::testing::_;
|
||||
using ::testing::AtMost;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::Return;
|
||||
namespace {
|
||||
|
||||
class LibraryBackendTest : public ::testing::Test {
|
||||
protected:
|
||||
virtual void SetUp() {
|
||||
backend_.reset(new MemoryLibraryBackend(NULL));
|
||||
|
||||
connection_name_ = "thread_" + QString::number(
|
||||
reinterpret_cast<quint64>(QThread::currentThread()));
|
||||
database_ = QSqlDatabase::database(connection_name_);
|
||||
}
|
||||
|
||||
void TearDown() {
|
||||
// Make sure Qt does not re-use the connection.
|
||||
database_ = QSqlDatabase();
|
||||
QSqlDatabase::removeDatabase(connection_name_);
|
||||
database_.reset(new MemoryDatabase);
|
||||
backend_.reset(new LibraryBackend(database_.get(), Library::kSongsTable,
|
||||
Library::kDirsTable, Library::kSubdirsTable));
|
||||
}
|
||||
|
||||
Song MakeDummySong(int directory_id) {
|
||||
@ -59,71 +49,10 @@ class LibraryBackendTest : public ::testing::Test {
|
||||
return ret;
|
||||
}
|
||||
|
||||
boost::scoped_ptr<Database> database_;
|
||||
boost::scoped_ptr<LibraryBackend> backend_;
|
||||
QString connection_name_;
|
||||
QSqlDatabase database_;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_UNIX
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
struct PerfTimer {
|
||||
PerfTimer(int iterations) : iterations_(iterations) {
|
||||
gettimeofday(&start_time_, NULL);
|
||||
}
|
||||
|
||||
~PerfTimer() {
|
||||
gettimeofday(&end_time_, NULL);
|
||||
|
||||
timeval elapsed_time;
|
||||
timersub(&end_time_, &start_time_, &elapsed_time);
|
||||
int elapsed_us = elapsed_time.tv_usec + elapsed_time.tv_sec * 1000000;
|
||||
|
||||
qDebug() << "Elapsed:" << elapsed_us << "us";
|
||||
qDebug() << "Time per iteration:" << float(elapsed_us) / iterations_ << "us";
|
||||
}
|
||||
|
||||
timeval start_time_;
|
||||
timeval end_time_;
|
||||
int iterations_;
|
||||
};
|
||||
|
||||
TEST_F(LibraryBackendTest, LikePerformance) {
|
||||
const int iterations = 1000000;
|
||||
|
||||
const char* needle = "foo";
|
||||
const char* haystack = "foobarbaz foobarbaz";
|
||||
qDebug() << "Simple query";
|
||||
{
|
||||
PerfTimer perf(iterations);
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
backend_->Like(needle, haystack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_F(LibraryBackendTest, DatabaseInitialises) {
|
||||
// Check that these tables exist
|
||||
QStringList tables = database_.tables();
|
||||
EXPECT_TRUE(tables.contains("songs"));
|
||||
EXPECT_TRUE(tables.contains("directories"));
|
||||
EXPECT_TRUE(tables.contains("subdirectories"));
|
||||
EXPECT_TRUE(tables.contains("playlists"));
|
||||
EXPECT_TRUE(tables.contains("playlist_items"));
|
||||
ASSERT_TRUE(tables.contains("schema_version"));
|
||||
|
||||
// Check the schema version is correct
|
||||
QSqlQuery q("SELECT version FROM schema_version", database_);
|
||||
ASSERT_TRUE(q.exec());
|
||||
ASSERT_TRUE(q.next());
|
||||
EXPECT_EQ(LibraryBackend::kSchemaVersion, q.value(0).toInt());
|
||||
EXPECT_FALSE(q.next());
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, EmptyDatabase) {
|
||||
// Check the database is empty to start with
|
||||
QStringList artists = backend_->GetAllArtists();
|
||||
@ -171,7 +100,7 @@ TEST_F(LibraryBackendTest, AddInvalidSong) {
|
||||
Song s;
|
||||
s.set_directory_id(1);
|
||||
|
||||
QSignalSpy spy(backend_.get(), SIGNAL(Error(QString)));
|
||||
QSignalSpy spy(database_.get(), SIGNAL(Error(QString)));
|
||||
|
||||
backend_->AddOrUpdateSongs(SongList() << s);
|
||||
ASSERT_EQ(1, spy.count()); spy.takeFirst();
|
||||
@ -196,37 +125,6 @@ TEST_F(LibraryBackendTest, AddInvalidSong) {
|
||||
TEST_F(LibraryBackendTest, GetAlbumArtNonExistent) {
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeWorksWithAllAscii) {
|
||||
EXPECT_TRUE(backend_->Like("%ar%", "bar"));
|
||||
EXPECT_FALSE(backend_->Like("%ar%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeWorksWithUnicode) {
|
||||
EXPECT_TRUE(backend_->Like("%Снег%", "Снег"));
|
||||
EXPECT_FALSE(backend_->Like("%Снег%", "foo"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeAsciiCaseInsensitive) {
|
||||
EXPECT_TRUE(backend_->Like("%ar%", "BAR"));
|
||||
EXPECT_FALSE(backend_->Like("%ar%", "FOO"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeUnicodeCaseInsensitive) {
|
||||
EXPECT_TRUE(backend_->Like("%снег%", "Снег"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeCacheInvalidated) {
|
||||
EXPECT_TRUE(backend_->Like("%foo%", "foobar"));
|
||||
EXPECT_FALSE(backend_->Like("%baz%", "foobar"));
|
||||
}
|
||||
|
||||
TEST_F(LibraryBackendTest, LikeQuerySplit) {
|
||||
EXPECT_TRUE(backend_->Like("%foo bar%", "foobar"));
|
||||
EXPECT_FALSE(backend_->Like("%foo bar%", "barbaz"));
|
||||
EXPECT_FALSE(backend_->Like("%foo bar%", "foobaz"));
|
||||
EXPECT_FALSE(backend_->Like("%foo bar%", "baz"));
|
||||
}
|
||||
|
||||
// Test adding a single song to the database, then getting various information
|
||||
// back about it.
|
||||
class SingleSong : public LibraryBackendTest {
|
||||
@ -398,3 +296,5 @@ TEST_F(SingleSong, DeleteSongs) {
|
||||
LibraryBackend::AlbumList albums = backend_->GetAllAlbums();
|
||||
EXPECT_EQ(0, albums.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
291
tests/librarymodel_test.cpp
Normal file
291
tests/librarymodel_test.cpp
Normal file
@ -0,0 +1,291 @@
|
||||
/* 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 "gtest/gtest.h"
|
||||
|
||||
#include "database.h"
|
||||
#include "librarymodel.h"
|
||||
#include "librarybackend.h"
|
||||
#include "library.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QThread>
|
||||
#include <QSignalSpy>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
namespace {
|
||||
|
||||
class LibraryModelTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() {
|
||||
database_.reset(new MemoryDatabase);
|
||||
backend_.reset(new LibraryBackend(database_.get(), Library::kSongsTable,
|
||||
Library::kDirsTable, Library::kSubdirsTable));
|
||||
model_.reset(new LibraryModel(backend_.get()));
|
||||
|
||||
added_dir_ = false;
|
||||
|
||||
model_sorted_.reset(new QSortFilterProxyModel);
|
||||
model_sorted_->setSourceModel(model_.get());
|
||||
model_sorted_->setSortRole(LibraryModel::Role_SortText);
|
||||
model_sorted_->setDynamicSortFilter(true);
|
||||
model_sorted_->sort(0);
|
||||
}
|
||||
|
||||
Song AddSong(Song& song) {
|
||||
song.set_directory_id(1);
|
||||
if (song.mtime() == -1) song.set_mtime(0);
|
||||
if (song.ctime() == -1) song.set_ctime(0);
|
||||
if (song.filename().isNull()) song.set_filename("/test/foo");
|
||||
if (song.filesize() == -1) song.set_filesize(0);
|
||||
|
||||
if (!added_dir_) {
|
||||
backend_->AddDirectory("/test");
|
||||
added_dir_ = true;
|
||||
}
|
||||
|
||||
backend_->AddOrUpdateSongs(SongList() << song);
|
||||
return song;
|
||||
}
|
||||
|
||||
Song AddSong(const QString& title, const QString& artist, const QString& album, int length) {
|
||||
Song song;
|
||||
song.Init(title, artist, album, length);
|
||||
return AddSong(song);
|
||||
}
|
||||
|
||||
boost::scoped_ptr<Database> database_;
|
||||
boost::scoped_ptr<LibraryBackend> backend_;
|
||||
boost::scoped_ptr<LibraryModel> model_;
|
||||
boost::scoped_ptr<QSortFilterProxyModel> model_sorted_;
|
||||
|
||||
bool added_dir_;
|
||||
};
|
||||
|
||||
TEST_F(LibraryModelTest, Initialisation) {
|
||||
EXPECT_EQ(0, model_->rowCount(QModelIndex()));
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, WithInitialArtists) {
|
||||
AddSong("Title", "Artist 1", "Album", 123);
|
||||
AddSong("Title", "Artist 2", "Album", 123);
|
||||
AddSong("Title", "Foo", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
ASSERT_EQ(5, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("A", model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist 1", model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist 2", model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("F", model_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Foo", model_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, CompilationAlbums) {
|
||||
Song song;
|
||||
song.Init("Title", "Artist", "Album", 123);
|
||||
song.set_compilation(true);
|
||||
|
||||
AddSong(song);
|
||||
model_->Init();
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
|
||||
|
||||
QModelIndex va_index = model_->index(0, 0, QModelIndex());
|
||||
EXPECT_EQ("Various Artists", va_index.data().toString());
|
||||
EXPECT_TRUE(model_->hasChildren(va_index));
|
||||
ASSERT_EQ(model_->rowCount(va_index), 1);
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, va_index);
|
||||
EXPECT_EQ(model_->data(album_index).toString(), "Album");
|
||||
EXPECT_TRUE(model_->hasChildren(album_index));
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, NumericHeaders) {
|
||||
AddSong("Title", "1artist", "Album", 123);
|
||||
AddSong("Title", "2artist", "Album", 123);
|
||||
AddSong("Title", "0artist", "Album", 123);
|
||||
AddSong("Title", "zartist", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
ASSERT_EQ(6, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("0-9", model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("0artist", model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("1artist", model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("2artist", model_sorted_->index(3, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Z", model_sorted_->index(4, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("zartist", model_sorted_->index(5, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, MixedCaseHeaders) {
|
||||
AddSong("Title", "Artist", "Album", 123);
|
||||
AddSong("Title", "artist", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
ASSERT_EQ(3, model_sorted_->rowCount(QModelIndex()));
|
||||
EXPECT_EQ("A", model_sorted_->index(0, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("Artist", model_sorted_->index(1, 0, QModelIndex()).data().toString());
|
||||
EXPECT_EQ("artist", model_sorted_->index(2, 0, QModelIndex()).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, UnknownArtists) {
|
||||
AddSong("Title", "", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
|
||||
QModelIndex unknown_index = model_->index(0, 0, QModelIndex());
|
||||
EXPECT_EQ("Unknown", unknown_index.data().toString());
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(unknown_index));
|
||||
EXPECT_EQ("Album", model_->index(0, 0, unknown_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, UnknownAlbums) {
|
||||
AddSong("Title", "Artist", "", 123);
|
||||
AddSong("Title", "Artist", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(2, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex unknown_album_index = model_->index(0, 0, artist_index);
|
||||
QModelIndex real_album_index = model_->index(1, 0, artist_index);
|
||||
|
||||
EXPECT_EQ("Unknown", unknown_album_index.data().toString());
|
||||
EXPECT_EQ("Album", real_album_index.data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, VariousArtistSongs) {
|
||||
SongList songs;
|
||||
for (int i=0 ; i<4 ; ++i) {
|
||||
QString n = QString::number(i+1);
|
||||
Song song;
|
||||
song.Init("Title " + n, "Artist " + n, "Album", 0);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
// Different ways of putting songs in "Various Artist". Make sure they all work
|
||||
songs[0].set_sampler(true);
|
||||
songs[1].set_compilation(true);
|
||||
songs[2].set_forced_compilation_on(true);
|
||||
songs[3].set_sampler(true); songs[3].set_artist("Various Artists");
|
||||
|
||||
for (int i=0 ; i<4 ; ++i)
|
||||
AddSong(songs[i]);
|
||||
model_->Init();
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(4, model_->rowCount(album_index));
|
||||
|
||||
EXPECT_EQ("Artist 1 - Title 1", model_->index(0, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Artist 2 - Title 2", model_->index(1, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Artist 3 - Title 3", model_->index(2, 0, album_index).data().toString());
|
||||
EXPECT_EQ("Title 4", model_->index(3, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, RemoveSongsLazyLoaded) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album", 123); two.set_id(2);
|
||||
AddSong("Title 3", "Artist", "Album", 123);
|
||||
model_->Init();
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(3, model_->rowCount(album_index));
|
||||
|
||||
// Remove the first two songs
|
||||
QSignalSpy spy_preremove(model_.get(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_remove(model_.get(), SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_reset(model_.get(), SIGNAL(modelReset()));
|
||||
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
ASSERT_EQ(2, spy_preremove.count());
|
||||
ASSERT_EQ(2, spy_remove.count());
|
||||
ASSERT_EQ(0, spy_reset.count());
|
||||
|
||||
artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
album_index = model_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
EXPECT_EQ("Title 3", model_->index(0, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, RemoveSongsNotLazyLoaded) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album", 123); two.set_id(2);
|
||||
model_->Init();
|
||||
|
||||
// Remove the first two songs
|
||||
QSignalSpy spy_preremove(model_.get(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_remove(model_.get(), SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy spy_reset(model_.get(), SIGNAL(modelReset()));
|
||||
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
ASSERT_EQ(0, spy_preremove.count());
|
||||
ASSERT_EQ(0, spy_remove.count());
|
||||
ASSERT_EQ(1, spy_reset.count());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, RemoveEmptyAlbums) {
|
||||
Song one = AddSong("Title 1", "Artist", "Album 1", 123); one.set_id(1);
|
||||
Song two = AddSong("Title 2", "Artist", "Album 2", 123); two.set_id(2);
|
||||
Song three = AddSong("Title 3", "Artist", "Album 2", 123); three.set_id(3);
|
||||
model_->Init();
|
||||
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(2, model_->rowCount(artist_index));
|
||||
|
||||
// Remove one song from each album
|
||||
backend_->DeleteSongs(SongList() << one << two);
|
||||
|
||||
// Check the model
|
||||
artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
EXPECT_EQ("Album 2", album_index.data().toString());
|
||||
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
EXPECT_EQ("Title 3", model_->index(0, 0, album_index).data().toString());
|
||||
}
|
||||
|
||||
TEST_F(LibraryModelTest, RemoveEmptyArtists) {
|
||||
Song one = AddSong("Title", "Artist", "Album", 123); one.set_id(1);
|
||||
model_->Init();
|
||||
|
||||
// Lazy load the items
|
||||
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
|
||||
ASSERT_EQ(1, model_->rowCount(artist_index));
|
||||
QModelIndex album_index = model_->index(0, 0, artist_index);
|
||||
ASSERT_EQ(1, model_->rowCount(album_index));
|
||||
|
||||
// The artist header is there too right?
|
||||
ASSERT_EQ(2, model_->rowCount(QModelIndex()));
|
||||
|
||||
// Remove the song
|
||||
backend_->DeleteSongs(SongList() << one);
|
||||
|
||||
// Everything should be gone - even the artist header
|
||||
ASSERT_EQ(0, model_->rowCount(QModelIndex()));
|
||||
}
|
||||
|
||||
} // namespace
|
@ -32,7 +32,7 @@ namespace {
|
||||
class PlaylistTest : public ::testing::Test {
|
||||
protected:
|
||||
PlaylistTest()
|
||||
: playlist_(NULL, new DummySettingsProvider),
|
||||
: playlist_(NULL, NULL, new DummySettingsProvider),
|
||||
sequence_(NULL, new DummySettingsProvider)
|
||||
{
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user