Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2914,6 +2914,7 @@ if(BUILD_TESTING)
src/test/queryutiltest.cpp
src/test/rangelist_test.cpp
src/test/readaheadmanager_test.cpp
src/test/rekordboxplaylistmodel_test.cpp
src/test/replaygaintest.cpp
src/test/rescalertest.cpp
src/test/rgbcolor_test.cpp
Expand Down
5 changes: 5 additions & 0 deletions src/library/rekordbox/rekordboxconstants.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#pragma once

#include <QString>

namespace mixxx {
namespace rekordboxconstants {
const QString beatsSubversion = QStringLiteral("Rekordbox USB drive");
// Stable key for column layout; independent of table/playlist lifecycle.
const QString kRekordboxHeaderStateKey =
QStringLiteral("mixxx.rekordbox.header_state_pb");
}
} // namespace mixxx
206 changes: 188 additions & 18 deletions src/library/rekordbox/rekordboxfeature.cpp

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/library/rekordbox/rekordboxfeature.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ class RekordboxPlaylistModel : public BaseExternalPlaylistModel {
TrackCollectionManager* pTrackCollectionManager,
QSharedPointer<BaseTrackCache> trackSource);
TrackPointer getTrack(const QModelIndex& index) const override;
Capabilities getCapabilities() const override;
bool isColumnHiddenByDefault(int column) override;
bool isColumnInternal(int column) override;

QString getModelSetting(const QString& name) override;
bool setModelSetting(const QString& name, const QVariant& value) override;

protected:
void initSortColumnMapping() override;
};
Expand Down
131 changes: 131 additions & 0 deletions src/test/rekordboxplaylistmodel_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#include <gtest/gtest.h>

#include <QSqlQuery>

#include "library/basetrackcache.h"
#include "library/columncache.h"
#include "library/coverart.h"
#include "library/dao/trackschema.h"
#include "library/rekordbox/rekordboxfeature.h"
#include "library/trackmodel.h"
#include "test/librarytest.h"

namespace {

const QString kLibraryTable = QStringLiteral("rekordbox_library");
const QString kLibraryView = QStringLiteral("rekordbox_library_view");
const QString kPlaylistsTable = QStringLiteral("rekordbox_playlists");
const QString kPlaylistTracksTable = QStringLiteral("rekordbox_playlist_tracks");

void createRekordboxTables(QSqlDatabase& db) {
QSqlQuery q(db);
ASSERT_TRUE(q.exec(
"CREATE TABLE " + kLibraryTable +
" (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" rb_id INTEGER, artist TEXT, title TEXT, album TEXT, year INTEGER,"
" genre TEXT, tracknumber TEXT, location TEXT UNIQUE, comment TEXT,"
" duration INTEGER, bitrate TEXT, bpm FLOAT, key TEXT, key_id INTEGER,"
" tuning_frequency_hz REAL, rating INTEGER, analyze_path TEXT UNIQUE,"
" device TEXT, color INTEGER,"
" coverart_type INTEGER, coverart_source INTEGER, coverart_location TEXT,"
" coverart_color INTEGER, coverart_digest BLOB, coverart_hash INTEGER)"));
ASSERT_TRUE(q.exec(
"CREATE TEMPORARY VIEW IF NOT EXISTS " + kLibraryView +
" AS SELECT *, coverart_digest AS coverart FROM " + kLibraryTable));
ASSERT_TRUE(q.exec(
"CREATE TABLE " + kPlaylistsTable +
" (id INTEGER PRIMARY KEY, name TEXT UNIQUE)"));
ASSERT_TRUE(q.exec(
"CREATE TABLE " + kPlaylistTracksTable +
" (id INTEGER PRIMARY KEY AUTOINCREMENT,"
" playlist_id INTEGER REFERENCES " + kPlaylistsTable + "(id),"
" track_id INTEGER REFERENCES " + kLibraryTable + "(id),"
" position INTEGER)"));
}

QSharedPointer<BaseTrackCache> makeTrackSource(TrackCollection* pCollection) {
const QStringList columns = {
LIBRARYTABLE_ID,
LIBRARYTABLE_ARTIST,
LIBRARYTABLE_TITLE,
TRACKLOCATIONSTABLE_LOCATION,
LIBRARYTABLE_COVERART_SOURCE,
LIBRARYTABLE_COVERART_TYPE,
LIBRARYTABLE_COVERART_LOCATION,
LIBRARYTABLE_COVERART_COLOR,
LIBRARYTABLE_COVERART_DIGEST,
LIBRARYTABLE_COVERART_HASH,
LIBRARYTABLE_COVERART,
};
return QSharedPointer<BaseTrackCache>::create(
pCollection,
kLibraryView,
LIBRARYTABLE_ID,
columns,
QStringList{LIBRARYTABLE_ARTIST, LIBRARYTABLE_TITLE},
false);
}

} // namespace

class RekordboxPlaylistModelTest : public LibraryTest {
protected:
void SetUp() override {
auto db = dbConnection();
createRekordboxTables(db);
m_pTrackSource = makeTrackSource(internalCollection());
m_pModel = std::make_unique<RekordboxPlaylistModel>(
nullptr, trackCollectionManager(), m_pTrackSource);
}

QSharedPointer<BaseTrackCache> m_pTrackSource;
std::unique_ptr<RekordboxPlaylistModel> m_pModel;
};

TEST_F(RekordboxPlaylistModelTest, LibraryViewProvidesCoverartAlias) {
auto db = dbConnection();
QSqlQuery q(db);

const QByteArray kDigest = QByteArray::fromHex("deadbeef");
ASSERT_TRUE(q.prepare(
"INSERT INTO " + kLibraryTable +
" (location, coverart_digest) VALUES (:loc, :digest)"));
q.bindValue(":loc", "/tmp/test.mp3");
q.bindValue(":digest", kDigest);
ASSERT_TRUE(q.exec());

ASSERT_TRUE(q.exec("SELECT coverart, coverart_digest FROM " + kLibraryView));
ASSERT_TRUE(q.next());
EXPECT_EQ(kDigest, q.value(0).toByteArray());
EXPECT_EQ(kDigest, q.value(1).toByteArray());
EXPECT_EQ(q.value(0).toByteArray(), q.value(1).toByteArray());
}

TEST_F(RekordboxPlaylistModelTest, GetCapabilitiesIncludesEditMetadata) {
const TrackModel::Capabilities caps = m_pModel->getCapabilities();
EXPECT_TRUE(caps.testFlag(TrackModel::Capability::EditMetadata));
}

TEST_F(RekordboxPlaylistModelTest, GetCapabilitiesPreservesBaseCapabilities) {
const TrackModel::Capabilities caps = m_pModel->getCapabilities();
EXPECT_TRUE(caps.testFlag(TrackModel::Capability::AddToTrackSet));
EXPECT_TRUE(caps.testFlag(TrackModel::Capability::AddToAutoDJ));
EXPECT_TRUE(caps.testFlag(TrackModel::Capability::LoadToDeck));
EXPECT_TRUE(caps.testFlag(TrackModel::Capability::Sorting));
}

TEST_F(RekordboxPlaylistModelTest, CoverartColumnHasValidFieldIndex) {
auto db = dbConnection();
QSqlQuery q(db);

ASSERT_TRUE(q.exec(
"INSERT INTO " + kPlaylistsTable + " (name) VALUES ('TestPlaylist')"));

m_pModel->setPlaylist("TestPlaylist");

const int coverartFieldIndex =
m_pModel->fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART);
EXPECT_GE(coverartFieldIndex, 0)
<< "COLUMN_LIBRARYTABLE_COVERART must map to a valid column index "
"so that CoverArtDelegate is created for the thumbnail column";
}
55 changes: 55 additions & 0 deletions src/test/tableview_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,58 @@ TEST_F(HeaderViewStateTest, BadHeaderState) {
HeaderViewState view_state("BLAHBLAHBLAHBAD");
ASSERT_FALSE(view_state.healthy());
}

TEST_F(HeaderViewStateTest, EmptyHeaderState) {
HeaderViewState view_state{mixxx::library::HeaderViewState()};
ASSERT_FALSE(view_state.healthy());
QString saved = view_state.saveState();
HeaderViewState loaded(saved);
ASSERT_EQ(saved, loaded.saveState());
}

TEST_F(HeaderViewStateTest, AllColumnsHidden) {
mixxx::library::HeaderViewState headerViewState_pb;
auto* header_state_pb = headerViewState_pb.add_header_state();
header_state_pb->set_hidden(true);
header_state_pb->set_column_name("Col1");
header_state_pb = headerViewState_pb.add_header_state();
header_state_pb->set_hidden(true);
header_state_pb->set_column_name("Col2");

HeaderViewState view_state(headerViewState_pb);
ASSERT_FALSE(view_state.healthy());
}

TEST_F(HeaderViewStateTest, SortIndicatorRoundTrip) {
mixxx::library::HeaderViewState headerViewState_pb;
auto* header_state_pb = headerViewState_pb.add_header_state();
header_state_pb->set_hidden(false);
header_state_pb->set_column_name("Artist");
headerViewState_pb.set_sort_indicator_shown(true);
headerViewState_pb.set_sort_indicator_section(2);
headerViewState_pb.set_sort_order(Qt::AscendingOrder);

HeaderViewState view_state(headerViewState_pb);
QString saved = view_state.saveState();
HeaderViewState loaded(saved);
ASSERT_EQ(saved, loaded.saveState());
ASSERT_TRUE(loaded.healthy());
}

TEST_F(HeaderViewStateTest, SortIndicatorNotShownRoundTrip) {
mixxx::library::HeaderViewState headerViewState_pb;
auto* header_state_pb = headerViewState_pb.add_header_state();
header_state_pb->set_hidden(false);
header_state_pb->set_column_name("Title");

HeaderViewState view_state(headerViewState_pb);
ASSERT_TRUE(view_state.healthy());
QString saved = view_state.saveState();
HeaderViewState loaded(saved);
ASSERT_EQ(saved, loaded.saveState());
}

TEST_F(HeaderViewStateTest, EmptyStringDeserialization) {
HeaderViewState view_state{QString()};
ASSERT_FALSE(view_state.healthy());
}
41 changes: 41 additions & 0 deletions src/widget/wcoverartmenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,46 @@

#include <QFileDialog>
#include <QFileInfo>
#include <QLabel>
#include <QWidgetAction>

#include "library/coverartcache.h"
#include "library/coverartutils.h"
#include "moc_wcoverartmenu.cpp"
#include "util/assert.h"

WCoverArtMenu::WCoverArtMenu(QWidget* parent)
: QMenu(parent),
m_pCoverPreviewAction(nullptr),
m_pCoverLabel(nullptr),
m_isWorkerRunning(false) {
createActions();
connect(CoverArtCache::instance(),
&CoverArtCache::coverFound,
this,
&WCoverArtMenu::slotCoverFound);
}

WCoverArtMenu::~WCoverArtMenu() {
delete m_pCoverPreviewAction;
delete m_pChange;
delete m_pReload;
delete m_pUnset;
}

void WCoverArtMenu::createActions() {
m_pCoverPreviewAction = new QWidgetAction(this);
m_pCoverLabel = new QLabel(this);
m_pCoverLabel->setFixedSize(kCoverPreviewSize, kCoverPreviewSize);
m_pCoverLabel->setAlignment(Qt::AlignCenter);
m_pCoverLabel->setScaledContents(false);
m_pCoverLabel->setStyleSheet(
QStringLiteral("QLabel { background-color: palette(base); border: 1px solid palette(mid); }"));
m_pCoverPreviewAction->setDefaultWidget(m_pCoverLabel);
addAction(m_pCoverPreviewAction);

addSeparator();

m_pChange = new QAction(tr("Choose file", "change cover art location"), this);
connect(m_pChange, &QAction::triggered, this, &WCoverArtMenu::slotChange);
addAction(m_pChange);
Expand All @@ -37,6 +59,25 @@ void WCoverArtMenu::createActions() {

void WCoverArtMenu::setCoverArt(const CoverInfo& coverInfo) {
m_coverInfo = coverInfo;
m_pCoverLabel->clear();
m_pCoverLabel->setToolTip(QString());
if (coverInfo.trackLocation.isEmpty()) {
return;
}
CoverArtCache::requestCover(this, coverInfo);
}

void WCoverArtMenu::slotCoverFound(const QObject* requester,
const CoverInfo& coverInfo,
const QPixmap& pixmap) {
if (requester != this || coverInfo != m_coverInfo || pixmap.isNull()) {
return;
}
QPixmap scaled = pixmap.scaled(kCoverPreviewSize,
kCoverPreviewSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
m_pCoverLabel->setPixmap(scaled);
}

void WCoverArtMenu::slotChange() {
Expand Down
11 changes: 10 additions & 1 deletion src/widget/wcoverartmenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "library/export/coverartcopyworker.h"

class QAction;
class QLabel;
class QWidgetAction;

// This class implements a context-menu with all CoverArt user actions. Callers
// must call setCoverArt before calling exec or popup. This class does
Expand All @@ -15,7 +17,7 @@ class QAction;
class WCoverArtMenu : public QMenu {
Q_OBJECT
public:
explicit WCoverArtMenu(QWidget *parent = nullptr);
explicit WCoverArtMenu(QWidget* parent = nullptr);
~WCoverArtMenu() override;

void setCoverArt(const CoverInfo& coverInfo);
Expand All @@ -33,10 +35,17 @@ class WCoverArtMenu : public QMenu {
void slotCoverArtCopyFailed(const QString& errorMessage);
void slotCoverArtUpdated(const CoverInfoRelative& coverInfo);
void slotFinished();
void slotCoverFound(const QObject* requester,
const CoverInfo& coverInfo,
const QPixmap& pixmap);

private:
void createActions();

static constexpr int kCoverPreviewSize = 96;

QWidgetAction* m_pCoverPreviewAction;
QLabel* m_pCoverLabel;
QAction* m_pChange;
QAction* m_pReload;
QAction* m_pUnset;
Expand Down