From ca3573b655c581d8cc9cab5f0f969f9967693eb0 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Fri, 31 Jan 2020 22:15:01 +0100 Subject: [PATCH 01/31] library: Add Serato library feature Related Bug Report: https://bugs.launchpad.net/mixxx/+bug/1845183 --- CMakeLists.txt | 1 + build/depends.py | 1 + res/images/library/ic_library_serato.svg | 25 + res/mixxx.qrc | 3 +- src/library/library.cpp | 5 + src/library/serato/seratofeature.cpp | 613 ++++++++++++++++++++ src/library/serato/seratofeature.h | 79 +++ src/preferences/dialog/dlgpreflibrary.cpp | 4 + src/preferences/dialog/dlgpreflibrarydlg.ui | 12 +- 9 files changed, 741 insertions(+), 2 deletions(-) create mode 100644 res/images/library/ic_library_serato.svg create mode 100644 src/library/serato/seratofeature.cpp create mode 100644 src/library/serato/seratofeature.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d8cd3a6e46e..d9d4482aef8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -394,6 +394,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/scanner/scannertask.cpp src/library/searchquery.cpp src/library/searchqueryparser.cpp + src/library/serato/seratofeature.cpp src/library/setlogfeature.cpp src/library/sidebarmodel.cpp src/library/songdownloader.cpp diff --git a/build/depends.py b/build/depends.py index f558598ac96..25ce01fcef4 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1081,6 +1081,7 @@ def sources(self, build): "src/library/itunes/itunesfeature.cpp", "src/library/traktor/traktorfeature.cpp", + "src/library/serato/seratofeature.cpp", "src/library/rekordbox/rekordboxfeature.cpp", "src/library/rekordbox/rekordbox_pdb.cpp", diff --git a/res/images/library/ic_library_serato.svg b/res/images/library/ic_library_serato.svg new file mode 100644 index 00000000000..b5a5c9380b7 --- /dev/null +++ b/res/images/library/ic_library_serato.svg @@ -0,0 +1,25 @@ + +image/svg+xml \ No newline at end of file diff --git a/res/mixxx.qrc b/res/mixxx.qrc index a9719f9e5d3..9b1833187c2 100644 --- a/res/mixxx.qrc +++ b/res/mixxx.qrc @@ -25,7 +25,8 @@ images/library/ic_library_recordings.svg images/library/ic_library_rhythmbox.svg images/library/ic_library_traktor.svg - images/library/ic_library_rekordbox.svg + images/library/ic_library_rekordbox.svg + images/library/ic_library_serato.svg images/mixxx_logo.svg images/mixxx_icon.svg images/mixxx-icon-logo-symbolic.svg diff --git a/src/library/library.cpp b/src/library/library.cpp index 518499f9ede..3bed66ca09c 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -32,6 +32,7 @@ #include "library/setlogfeature.h" #include "library/traktor/traktorfeature.h" #include "library/rekordbox/rekordboxfeature.h" +#include "library/serato/seratofeature.h" #include "library/analysisfeature.h" #include "mixer/playermanager.h" @@ -168,6 +169,10 @@ Library::Library( addFeature(new RekordboxFeature(this, m_pConfig)); } + if (m_pConfig->getValue(ConfigKey(kConfigGroup, "ShowSeratoLibrary"), true)) { + addFeature(new SeratoFeature(this, m_pConfig)); + } + for (const auto& externalTrackCollection : m_pTrackCollectionManager->externalCollections()) { auto feature = externalTrackCollection->newLibraryFeature(this, m_pConfig); if (feature) { diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp new file mode 100644 index 00000000000..b76770db174 --- /dev/null +++ b/src/library/serato/seratofeature.cpp @@ -0,0 +1,613 @@ +// seratofeature.cpp +// Created 2020-01-31 by Jan Holthuis + +#include "library/serato/seratofeature.h" + +#include +#include +#include +#include +#include + +#include "engine/engine.h" +#include "library/library.h" +#include "library/librarytablemodel.h" +#include "library/missingtablemodel.h" +#include "library/queryutil.h" +#include "library/trackcollection.h" +#include "library/trackcollectionmanager.h" +#include "library/treeitem.h" +#include "track/beatfactory.h" +#include "track/cue.h" +#include "track/keyfactory.h" +#include "util/color/color.h" +#include "util/db/dbconnectionpooled.h" +#include "util/db/dbconnectionpooler.h" +#include "util/file.h" +#include "util/sandbox.h" +#include "waveform/waveform.h" +#include "widget/wlibrary.h" +#include "widget/wlibrarytextbrowser.h" + +namespace { + +enum class FieldId : quint32 { + Version = 0x7672736e, // vrsn + Track = 0x6f74726b, // otrk + FileType = 0x74747970, // ttyp + FilePath = 0x7066696c, // pfil + SongTitle = 0x74736e67, // tsng + Length = 0x746c656e, // tlen + Bitrate = 0x74626974, // tbit + SampleRate = 0x74736d70, // tsmp + Bpm = 0x7462706d, // tbpm + DateAddedText = 0x74616464, // tadd + DateAdded = 0x75616464, // uadd + Key = 0x746b6579, // tkey + BeatgridLocked = 0x6262676c, // bbgl + Artist = 0x74617274, // tart + FileTime = 0x75746d65, // utme + Missing = 0x626d6973, // bmis + Sorting = 0x7472736f, // osrt + ReverseOrder = 0x62726576, // brev + ColumnTitle = 0x6f766374, // ovct + ColumnName = 0x7476636e, // tvcn + ColumnWidth = 0x74766377, // tvcw + TrackPath = 0x7074726b, // ptrk +}; + +struct serato_track_t { + QString filetype; + QString location; + QString title; + QString duration; + QString bitrate; + QString samplerate; + QString bpm; + QString key; + QString artist; + bool beatgridlocked = false; + bool missing = false; + quint32 filetime = 0; + quint32 dateadded = 0; +}; + +const QString kDatabaseDirectory = "_Serato_"; +const QString kDatabaseFilename = "database V2"; +const QString kCrateFilter = "Subcrates/*.crate"; +const QString kSmartCrateFilter = "Smart Crates/*.scrate"; + +inline QString parseText(const QByteArray& data, const quint32 size) { + return QTextCodec::codecForName("UTF-16BE")->toUnicode(data, size); +} + +inline bool parseBoolean(const QByteArray& data) { + return data.at(0) != 0; +} + +inline quint32 parseUInt32(const QByteArray& data) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + return qFromBigEndian(data); +#else + return qFromBigEndian( + reinterpret_cast(data.constData())); +#endif +} + +inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { + QByteArray headerData = buffer.read(8); + while (headerData.length() == 8) { + QString fieldName = QString(headerData.mid(0, 4)); + quint32 fieldId = parseUInt32(headerData.mid(0, 4)); + quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + + // Read field data + QByteArray data = buffer.read(fieldSize); + if (static_cast(data.length()) != fieldSize) { + qWarning() << "Failed to read " + << fieldSize + << " bytes for " + << fieldName + << " field."; + return false; + } + + // Parse field data + switch (static_cast(fieldId)) { + case FieldId::FileType: + track.filetype = parseText(data, fieldSize); + break; + case FieldId::FilePath: + track.location = parseText(data, fieldSize); + break; + case FieldId::SongTitle: + track.title = parseText(data, fieldSize); + break; + case FieldId::Length: + track.duration = parseText(data, fieldSize); + break; + case FieldId::Bitrate: + track.bitrate = parseText(data, fieldSize); + break; + case FieldId::SampleRate: + track.samplerate = parseText(data, fieldSize); + break; + case FieldId::Bpm: + track.bpm = parseText(data, fieldSize); + break; + case FieldId::Key: + track.key = parseText(data, fieldSize); + break; + case FieldId::Artist: + track.artist = parseText(data, fieldSize); + break; + case FieldId::BeatgridLocked: + track.beatgridlocked = parseBoolean(data); + break; + case FieldId::Missing: + track.missing = parseBoolean(data); + break; + case FieldId::FileTime: + track.filetime = parseUInt32(data); + break; + case FieldId::DateAdded: + track.dateadded = parseUInt32(data); + break; + case FieldId::DateAddedText: + // Ignore this field, but do not print a debug message + break; + default: + qDebug() << "Ignoring unknown field " + << fieldName + << " (" + << fieldSize + << " bytes)."; + } + + headerData = buffer.read(8); + } + + if (headerData.length() != 0) { + qWarning() << "Found " + << headerData.length() + << " extra bytes at end of track definition."; + return false; + } + + return true; +} + +QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* databaseItem) { + QString databaseName = databaseItem->getLabel(); + QDir databaseDir = QDir(databaseItem->getData().toString()); + QString databaseFilePath = databaseDir.filePath(kDatabaseFilename); + + qWarning() << "Parsing database" + << databaseName + << "at" << databaseFilePath; + + if (!QFile(databaseFilePath).exists()) { + qWarning() << "Serato database file not found: " + << databaseFilePath; + return databaseFilePath; + } + + // The pooler limits the lifetime all thread-local connections, + // that should be closed immediately before exiting this function. + const mixxx::DbConnectionPooler dbConnectionPooler(dbConnectionPool); + QSqlDatabase database = mixxx::DbConnectionPooled(dbConnectionPool); + + //Open the database connection in this thread. + VERIFY_OR_DEBUG_ASSERT(database.isOpen()) { + qWarning() << "Failed to open database for Serato parser." + << database.lastError(); + return QString(); + } + + //Give thread a low priority + QThread* thisThread = QThread::currentThread(); + thisThread->setPriority(QThread::LowPriority); + + ScopedTransaction transaction(database); + + QFile databaseFile = QFile(databaseFilePath); + if (!databaseFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file " + << databaseFilePath + << " for reading."; + return QString(); + } + + QByteArray headerData = databaseFile.read(8); + while (headerData.length() == 8) { + QString fieldName = QString(headerData.mid(0, 4)); + quint32 fieldId = parseUInt32(headerData.mid(0, 4)); + quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + + // Read field data + QByteArray data = databaseFile.read(fieldSize); + if (static_cast(data.length()) != fieldSize) { + qWarning() << "Failed to read " + << fieldSize + << " bytes for " + << fieldName + << " field from " + << databaseFilePath + << "."; + return QString(); + } + + // Parse field data + qWarning() << "FieldId: " << fieldId; + switch (static_cast(fieldId)) { + case FieldId::Version: { + QString version = parseText(data, fieldSize); + qWarning() << "Serato Database Version: " + << version; + break; + } + case FieldId::Track: { + serato_track_t track; + QBuffer buffer = QBuffer(&data); + buffer.open(QIODevice::ReadOnly); + if (parseTrack(track, buffer)) { + qWarning() << "Track: " << track.location; + // TODO + } + break; + } + default: + qDebug() << "Ignoring unknown field " + << fieldName + << " (" + << fieldSize + << " bytes) in database " + << databaseFilePath + << "."; + } + + headerData = databaseFile.read(8); + } + + if (headerData.length() != 0) { + qWarning() << "Found " + << headerData.length() + << " extra bytes at end of Serato database file " + << databaseFilePath + << "."; + } + + return databaseFilePath; +} + +// This function is executed in a separate thread other than the main thread +QList findSeratoDatabases(SeratoFeature* seratoFeature) { + QThread* thisThread = QThread::currentThread(); + thisThread->setPriority(QThread::LowPriority); + + QList foundDatabases; + + // Build a list of directories that could contain the _Serato_ directory + QFileInfoList databaseLocations; + foreach (const QString& musicDir, QStandardPaths::standardLocations(QStandardPaths::MusicLocation)) { + databaseLocations.append(QFileInfo(musicDir)); + } +#if defined(__WINDOWS__) + // Repopulate drive list + // Using drive.filePath() instead of drive.canonicalPath() as it + // freezes interface too much if there is a network share mounted + // (drive letter assigned) but unavailable + // + // drive.canonicalPath() make a system call to the underlying filesystem + // introducing delay if it is unreadable. + // drive.filePath() doesn't make any access to the filesystem and consequently + // shorten the delay + databaseLocations.append(QDir::drives()); +#elif defined(__LINUX__) + // To get devices on Linux, we look for directories under /media and + // /run/media/$USER. + + // Add folders under /media to devices. + databaseLocations += QDir(QStringLiteral("/media")).entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot); + + // Add folders under /media/$USER to devices. + QDir mediaUserDir(QStringLiteral("/media/") + QString::fromLocal8Bit(qgetenv("USER"))); + databaseLocations += mediaUserDir.entryInfoList( + QDir::AllDirs | QDir::NoDotAndDotDot); + + // Add folders under /run/media/$USER to devices. + QDir runMediaUserDir(QStringLiteral("/run/media/") + QString::fromLocal8Bit(qgetenv("USER"))); + databaseLocations += runMediaUserDir.entryInfoList( + QDir::AllDirs | QDir::NoDotAndDotDot); +#elif defined(__APPLE__) + databaseLocations.append(QDir(QStringLiteral("/Volumes")).entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); +#endif + + foreach (QFileInfo databaseLocation, databaseLocations) { + QDir databaseDir = QDir(databaseLocation.filePath()); + if (!databaseDir.cd(kDatabaseDirectory)) { + continue; + } + + if (!databaseDir.exists(kDatabaseFilename)) { + continue; + } + + TreeItem* foundDatabase = new TreeItem(seratoFeature); + + QString displayPath = databaseLocation.filePath(); + if (displayPath.endsWith("/")) { + displayPath.chop(1); + } + + foundDatabase->setLabel(displayPath); + foundDatabase->setData(QVariant(databaseDir.path())); + + foundDatabases << foundDatabase; + } + + return foundDatabases; +} + +} // anonymous namespace + +SeratoPlaylistModel::SeratoPlaylistModel(QObject* parent, + TrackCollectionManager* trackCollectionManager, + QSharedPointer trackSource) + : BaseExternalPlaylistModel(parent, trackCollectionManager, "mixxx.db.model.serato.playlistmodel", "serato_playlists", "serato_playlist_tracks", trackSource) { +} + +void SeratoPlaylistModel::initSortColumnMapping() { + // Add a bijective mapping between the SortColumnIds and column indices + for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { + m_columnIndexBySortColumnId[i] = -1; + } + + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + + m_sortColumnIdByColumnIndex.clear(); + for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { + TrackModel::SortColumnId sortColumn = static_cast(i); + m_sortColumnIdByColumnIndex.insert(m_columnIndexBySortColumnId[sortColumn], sortColumn); + } +} + +TrackPointer SeratoPlaylistModel::getTrack(const QModelIndex& index) const { + qDebug() << "SeratoTrackModel::getTrack"; + + TrackPointer track = BaseExternalPlaylistModel::getTrack(index); + + return track; +} + +bool SeratoPlaylistModel::isColumnHiddenByDefault(int column) { + if ( + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID)) { + return true; + } + return BaseSqlTableModel::isColumnHiddenByDefault(column); +} + +SeratoFeature::SeratoFeature( + Library* pLibrary, + UserSettingsPointer pConfig) + : BaseExternalLibraryFeature(pLibrary, pConfig), + m_icon(":/images/library/ic_library_serato.svg") { + m_title = tr("Serato"); + + connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, this, &SeratoFeature::onSeratoDatabasesFound); + connect(&m_tracksFutureWatcher, &QFutureWatcher::finished, this, &SeratoFeature::onTracksFound); + // initialize the model + m_childModel.setRootItem(std::make_unique(this)); +} + +SeratoFeature::~SeratoFeature() { + m_databasesFuture.waitForFinished(); + m_tracksFuture.waitForFinished(); + delete m_pSeratoPlaylistModel; +} + +void SeratoFeature::bindLibraryWidget(WLibrary* libraryWidget, + KeyboardEventFilter* keyboard) { + Q_UNUSED(keyboard); + WLibraryTextBrowser* edit = new WLibraryTextBrowser(libraryWidget); + edit->setHtml(formatRootViewHtml()); + edit->setOpenLinks(false); + connect(edit, SIGNAL(anchorClicked(const QUrl)), this, SLOT(htmlLinkClicked(const QUrl))); + libraryWidget->registerView("SERATOHOME", edit); +} + +void SeratoFeature::htmlLinkClicked(const QUrl& link) { + if (QString(link.path()) == "refresh") { + activate(); + } else { + qDebug() << "Unknown link clicked" << link; + } +} + +BaseSqlTableModel* SeratoFeature::getPlaylistModelForPlaylist(QString playlist) { + SeratoPlaylistModel* model = new SeratoPlaylistModel(this, m_pLibrary->trackCollections(), m_trackSource); + model->setPlaylist(playlist); + return model; +} + +QVariant SeratoFeature::title() { + return m_title; +} + +QIcon SeratoFeature::getIcon() { + return m_icon; +} + +bool SeratoFeature::isSupported() { + return true; +} + +TreeItemModel* SeratoFeature::getChildModel() { + return &m_childModel; +} + +QString SeratoFeature::formatRootViewHtml() const { + QString title = tr("Serato"); + QString summary = tr("Reads the following from Serato the Music directory and removable devices:"); + QStringList items; + + items << tr("Absolutely nothing yet :("); + + QString html; + QString refreshLink = tr("Check for Serato databases (refresh)"); + html.append(QString("

%1

").arg(title)); + html.append(QString("

%1

").arg(summary)); + html.append(QString("
    ")); + for (const auto& item : items) { + html.append(QString("
  • %1
  • ").arg(item)); + } + html.append(QString("
")); + + //Colorize links in lighter blue, instead of QT default dark blue. + //Links are still different from regular text, but readable on dark/light backgrounds. + //https://bugs.launchpad.net/mixxx/+bug/1744816 + html.append(QString("%1") + .arg(refreshLink)); + return html; +} + +void SeratoFeature::refreshLibraryModels() { +} + +void SeratoFeature::activate() { + qDebug() << "SeratoFeature::activate()"; + + // Let a worker thread do the XML parsing + m_databasesFuture = QtConcurrent::run(findSeratoDatabases, this); + m_databasesFutureWatcher.setFuture(m_databasesFuture); + m_title = tr("(loading) Serato"); + //calls a slot in the sidebar model such that 'Serato (isLoading)' is displayed. + emit featureIsLoading(this, true); + + emit enableCoverArtDisplay(true); + emit switchToView("SERATOHOME"); +} + +void SeratoFeature::activateChild(const QModelIndex& index) { + if (!index.isValid()) + return; + + //access underlying TreeItem object + TreeItem* item = static_cast(index.internalPointer()); + if (!(item && item->getData().isValid())) { + return; + } + + // TreeItem list data holds 2 values in a QList and have different meanings. + // If the 2nd QList element IS_RECORDBOX_DEVICE, the 1st element is the + // filesystem device path, and the parseDeviceDB concurrent thread to parse + // the Rekcordbox database is initiated. If the 2nd element is + // IS_NOT_RECORDBOX_DEVICE, the 1st element is the playlist path and it is + // activated. + QList data = item->getData().toList(); + + qDebug() << "SeratoFeature::activateChild " << item->getLabel(); + + // Let a worker thread do the XML parsing + m_tracksFuture = QtConcurrent::run(parseDatabase, static_cast(parent())->dbConnectionPool(), item); + m_tracksFutureWatcher.setFuture(m_tracksFuture); + + // This device is now a playlist element, future activations should treat is + // as such + //item->setData(QVariant(data)); +} + +void SeratoFeature::onSeratoDatabasesFound() { + QList foundDatabases = m_databasesFuture.result(); + TreeItem* root = m_childModel.getRootItem(); + + QSqlDatabase database = m_pTrackCollection->database(); + + if (foundDatabases.size() == 0) { + // No Serato databases found + ScopedTransaction transaction(database); + transaction.commit(); + + if (root->childRows() > 0) { + // Devices have since been unmounted + m_childModel.removeRows(0, root->childRows()); + } + } else { + for (int databaseIndex = 0; databaseIndex < root->childRows(); databaseIndex++) { + TreeItem* child = root->child(databaseIndex); + bool removeChild = true; + + for (int foundDatabaseIndex = 0; foundDatabaseIndex < foundDatabases.size(); foundDatabaseIndex++) { + TreeItem* databaseFound = foundDatabases[foundDatabaseIndex]; + + if (databaseFound->getLabel() == child->getLabel()) { + removeChild = false; + break; + } + } + + if (removeChild) { + // Device has since been unmounted, cleanup DB + + m_childModel.removeRows(databaseIndex, 1); + } + } + + QList childrenToAdd; + + for (int foundDatabaseIndex = 0; foundDatabaseIndex < foundDatabases.size(); foundDatabaseIndex++) { + TreeItem* databaseFound = foundDatabases[foundDatabaseIndex]; + bool addNewChild = true; + + for (int databaseIndex = 0; databaseIndex < root->childRows(); databaseIndex++) { + TreeItem* child = root->child(databaseIndex); + + if (databaseFound->getLabel() == child->getLabel()) { + // This database already exists in the TreeModel, don't add or parse is again + addNewChild = false; + } + } + + if (addNewChild) { + childrenToAdd << databaseFound; + } + } + + if (!childrenToAdd.empty()) { + m_childModel.insertTreeItemRows(childrenToAdd, 0); + } + } + + // calls a slot in the sidebarmodel such that 'isLoading' is removed from the feature title. + m_title = tr("Serato"); + emit featureLoadingFinished(this); +} + +void SeratoFeature::onTracksFound() { + qDebug() << "onTracksFound"; + m_childModel.triggerRepaint(); +} diff --git a/src/library/serato/seratofeature.h b/src/library/serato/seratofeature.h new file mode 100644 index 00000000000..fa34459a1eb --- /dev/null +++ b/src/library/serato/seratofeature.h @@ -0,0 +1,79 @@ +#pragma once +// seratofeature.h +// Created 2020-01-31 by Jan Holthuis +// +// This feature reads tracks and crates from removable Serato Libraries, +// either in the Music directory or on removable devices (USB drives, etc), +// by parsing the contents of the _Serato_ directory on each device. +// +// Most of the groundwork for this has been done here: +// +// https://github.com/Holzhaus/serato-tags +// https://github.com/Holzhaus/serato-tags/blob/master/scripts/database_v2.py + +#include +#include +#include +#include +#include + +#include "library/baseexternallibraryfeature.h" +#include "library/baseexternalplaylistmodel.h" +#include "library/baseexternaltrackmodel.h" +#include "library/treeitemmodel.h" + +class TrackCollectionManager; +class BaseExternalPlaylistModel; + +class SeratoPlaylistModel : public BaseExternalPlaylistModel { + public: + SeratoPlaylistModel(QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + QSharedPointer trackSource); + TrackPointer getTrack(const QModelIndex& index) const override; + bool isColumnHiddenByDefault(int column) override; + + protected: + void initSortColumnMapping() override; +}; + +class SeratoFeature : public BaseExternalLibraryFeature { + Q_OBJECT + public: + SeratoFeature(Library* pLibrary, UserSettingsPointer pConfig); + ~SeratoFeature() override; + + QVariant title() override; + QIcon getIcon() override; + static bool isSupported(); + void bindLibraryWidget(WLibrary* libraryWidget, + KeyboardEventFilter* keyboard) override; + + TreeItemModel* getChildModel() override; + + public slots: + void activate() override; + void activateChild(const QModelIndex& index) override; + void refreshLibraryModels(); + void onSeratoDatabasesFound(); + void onTracksFound(); + + private slots: + void htmlLinkClicked(const QUrl& link); + + private: + QString formatRootViewHtml() const; + BaseSqlTableModel* getPlaylistModelForPlaylist(QString playlist) override; + + TreeItemModel m_childModel; + SeratoPlaylistModel* m_pSeratoPlaylistModel; + + QFutureWatcher> m_databasesFutureWatcher; + QFuture> m_databasesFuture; + QFutureWatcher m_tracksFutureWatcher; + QFuture m_tracksFuture; + QString m_title; + + QSharedPointer m_trackSource; + QIcon m_icon; +}; diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 4c7d6faff66..3baacf28dff 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -171,6 +171,8 @@ void DlgPrefLibrary::slotUpdate() { ConfigKey("[Library]","ShowTraktorLibrary"), true)); checkBox_show_rekordbox->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowRekordboxLibrary"), true)); + checkBox_show_serato->setChecked(m_pConfig->getValue( + ConfigKey("[Library]","ShowSeratoLibrary"), true)); switch (m_pConfig->getValue( ConfigKey("[Library]","TrackLoadAction"), LOAD_TO_DECK)) { @@ -311,6 +313,8 @@ void DlgPrefLibrary::slotApply() { ConfigValue((int)checkBox_show_traktor->isChecked())); m_pConfig->set(ConfigKey("[Library]","ShowRekordboxLibrary"), ConfigValue((int)checkBox_show_rekordbox->isChecked())); + m_pConfig->set(ConfigKey("[Library]","ShowSeratoLibrary"), + ConfigValue((int)checkBox_show_serato->isChecked())); int dbclick_status; if (radioButton_dbclick_bottom->isChecked()) { dbclick_status = ADD_TO_AUTODJ_BOTTOM; diff --git a/src/preferences/dialog/dlgpreflibrarydlg.ui b/src/preferences/dialog/dlgpreflibrarydlg.ui index c9ebc06d15e..38dde4c4f9b 100644 --- a/src/preferences/dialog/dlgpreflibrarydlg.ui +++ b/src/preferences/dialog/dlgpreflibrarydlg.ui @@ -337,6 +337,16 @@ + + + + Show Serato Library + + + true + + + @@ -344,7 +354,7 @@ - + Qt::Horizontal From cd541e2954771799cfddd34081b11ad98f3df1c0 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 1 Feb 2020 01:02:05 +0100 Subject: [PATCH 02/31] library/serato/seratofeature: Improve database track field parsing --- src/library/serato/seratofeature.cpp | 71 +++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index b76770db174..3fe33227b76 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -31,12 +31,23 @@ namespace { +// Serato Database Field IDs +// The "magic" value is the short 4 byte ascii code intepreted as quint32, so +// that we can use the value in a switch statement instead of going through +// a strcmp if/else ladder. enum class FieldId : quint32 { Version = 0x7672736e, // vrsn Track = 0x6f74726b, // otrk FileType = 0x74747970, // ttyp FilePath = 0x7066696c, // pfil SongTitle = 0x74736e67, // tsng + Artist = 0x74617274, // tart + Album = 0x74616c62, // talb + Genre = 0x7467656e, // tgen + Comment = 0x74636f6d, // tcom + Grouping = 0x74677270, // tgrp + Label = 0x746c626c, // tlbl + Year = 0x74747972, // ttyr Length = 0x746c656e, // tlen Bitrate = 0x74626974, // tbit SampleRate = 0x74736d70, // tsmp @@ -45,7 +56,6 @@ enum class FieldId : quint32 { DateAdded = 0x75616464, // uadd Key = 0x746b6579, // tkey BeatgridLocked = 0x6262676c, // bbgl - Artist = 0x74617274, // tart FileTime = 0x75746d65, // utme Missing = 0x626d6973, // bmis Sorting = 0x7472736f, // osrt @@ -60,12 +70,18 @@ struct serato_track_t { QString filetype; QString location; QString title; - QString duration; + QString artist; + QString album; + QString genre; + QString comment; + QString grouping; + QString label; + int year = -1; + int duration = -1; QString bitrate; QString samplerate; - QString bpm; + double bpm = -1.0; QString key; - QString artist; bool beatgridlocked = false; bool missing = false; quint32 filetime = 0; @@ -123,24 +139,57 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { case FieldId::SongTitle: track.title = parseText(data, fieldSize); break; - case FieldId::Length: - track.duration = parseText(data, fieldSize); + case FieldId::Artist: + track.artist = parseText(data, fieldSize); break; + case FieldId::Album: + track.album = parseText(data, fieldSize); + break; + case FieldId::Genre: + track.genre = parseText(data, fieldSize); + break; + case FieldId::Length: { + bool ok; + int duration = parseText(data, fieldSize).toInt(&ok); + if (ok) { + track.duration = duration; + } + break; + } case FieldId::Bitrate: track.bitrate = parseText(data, fieldSize); break; case FieldId::SampleRate: track.samplerate = parseText(data, fieldSize); break; - case FieldId::Bpm: - track.bpm = parseText(data, fieldSize); + case FieldId::Bpm: { + bool ok; + double bpm = parseText(data, fieldSize).toDouble(&ok); + if (ok) { + track.bpm = bpm; + } + break; + } + case FieldId::Comment: + track.comment = parseText(data, fieldSize); break; + case FieldId::Grouping: + track.grouping = parseText(data, fieldSize); + break; + case FieldId::Label: + track.label = parseText(data, fieldSize); + break; + case FieldId::Year: { + bool ok; + int year = parseText(data, fieldSize).toInt(&ok); + if (ok) { + track.year = year; + } + break; + } case FieldId::Key: track.key = parseText(data, fieldSize); break; - case FieldId::Artist: - track.artist = parseText(data, fieldSize); - break; case FieldId::BeatgridLocked: track.beatgridlocked = parseBoolean(data); break; From 144e8cbe6599fac342ae5712f41afd2bf65d9df5 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 1 Feb 2020 04:14:31 +0100 Subject: [PATCH 03/31] library/serato/seratofeature: Add tracks to library playlist --- res/schema.xml | 44 +++++ src/database/mixxxdb.cpp | 2 +- src/library/serato/seratofeature.cpp | 244 ++++++++++++++++++++++++--- 3 files changed, 264 insertions(+), 26 deletions(-) diff --git a/res/schema.xml b/res/schema.xml index d5babba85aa..a57b3b16f16 100644 --- a/res/schema.xml +++ b/res/schema.xml @@ -481,4 +481,48 @@ METADATA ); + + + Tables for Serato library feature + + + CREATE TABLE IF NOT EXISTS serato_library ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + artist TEXT, + album TEXT, + genre TEXT, + comment TEXT, + grouping TEXT, + year INTEGER, + duration INTEGER, + bitrate TEXT, + samplerate TEXT, + bpm FLOAT, + key TEXT, + location TEXT, + bpm_lock INTEGER, + datetime_added DEFAULT CURRENT_TIMESTAMP, + label TEXT, + composer TEXT, + filename TEXT, + filetype TEXT, + remixer TEXT, + size INTEGER, + tracknumber TEXT, + serato_db TEXT + ); + CREATE TABLE IF NOT EXISTS serato_playlists ( + id INTEGER PRIMARY KEY, + name TEXT, + serato_db TEXT + ); + CREATE TABLE IF NOT EXISTS serato_playlist_tracks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + playlist_id INTEGER REFERENCES serato_playlists(id), + track_id INTEGER REFERENCES serato_library(id), + position INTEGER + ); + + diff --git a/src/database/mixxxdb.cpp b/src/database/mixxxdb.cpp index 5a18ff73526..6dcd79a0a52 100644 --- a/src/database/mixxxdb.cpp +++ b/src/database/mixxxdb.cpp @@ -11,7 +11,7 @@ const QString MixxxDb::kDefaultSchemaFile(":/schema.xml"); //static -const int MixxxDb::kRequiredSchemaVersion = 30; +const int MixxxDb::kRequiredSchemaVersion = 31; namespace { diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 3fe33227b76..4def1bcea25 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -17,6 +17,7 @@ #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "library/treeitem.h" +#include "library/dao/trackschema.h" #include "track/beatfactory.h" #include "track/cue.h" #include "track/keyfactory.h" @@ -85,13 +86,52 @@ struct serato_track_t { bool beatgridlocked = false; bool missing = false; quint32 filetime = 0; - quint32 dateadded = 0; + quint32 datetimeadded = 0; }; const QString kDatabaseDirectory = "_Serato_"; const QString kDatabaseFilename = "database V2"; -const QString kCrateFilter = "Subcrates/*.crate"; -const QString kSmartCrateFilter = "Smart Crates/*.scrate"; +const QString kCrateDirectory = "Subcrates"; +const QString kCrateFilter = "*.crate"; +const QString kSmartCrateDirectory = "Smart Crates"; +const QString kSmartCrateFilter = "*.scrate"; + +const QString kSeratoLibraryTable = "serato_library"; +const QString kSeratoPlaylistsTable = "serato_library"; +const QString kSeratoPlaylistTracksTable = "serato_library"; + +int createPlaylist(QSqlDatabase& database, QString name, QString databasePath) { + QSqlQuery query(database); + query.prepare( + "INSERT INTO serato_playlists (name, serato_db)" + "VALUES (:name, :serato_db)"); + query.bindValue(":name", name); + query.bindValue(":serato_db", databasePath); + + if (!query.exec()) { + LOG_FAILED_QUERY(query) << "databasePath: " << databasePath; + return -1; + } + + return query.lastInsertId().toInt(); +} + +int insertTrackIntoPlaylist(QSqlDatabase& database, int playlistId, int trackId, int position) { + QSqlQuery query(database); + query.prepare( + "INSERT INTO serato_playlist_tracks (playlist_id, track_id, position) " + "VALUES (:playlist_id, :track_id, :position)"); + query.bindValue(":playlist_id", playlistId); + query.bindValue(":track_id", trackId); + query.bindValue(":position", position); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return -1; + } + + return query.lastInsertId().toInt(); +} inline QString parseText(const QByteArray& data, const quint32 size) { return QTextCodec::codecForName("UTF-16BE")->toUnicode(data, size); @@ -200,7 +240,7 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { track.filetime = parseUInt32(data); break; case FieldId::DateAdded: - track.dateadded = parseUInt32(data); + track.datetimeadded = parseUInt32(data); break; case FieldId::DateAddedText: // Ignore this field, but do not print a debug message @@ -223,6 +263,11 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { return false; } + if (track.location.isEmpty()) { + qWarning() << "Found track with empty location field."; + return false; + } + return true; } @@ -231,9 +276,19 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QDir databaseDir = QDir(databaseItem->getData().toString()); QString databaseFilePath = databaseDir.filePath(kDatabaseFilename); - qWarning() << "Parsing database" - << databaseName - << "at" << databaseFilePath; + QDir databaseRootDir = QDir(databaseDir); + databaseRootDir.cdUp(); + +#if defined(__WINDOWS__) + // Find drive letter (paths are relative to drive root on Windows) + while (databaseRootDir.cdUp()) { + // Nothing to do here + } +#endif + + qDebug() << "Parsing Serato database" + << databaseName + << "at" << databaseFilePath; if (!QFile(databaseFilePath).exists()) { qWarning() << "Serato database file not found: " @@ -259,6 +314,46 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat ScopedTransaction transaction(database); + QSqlQuery query(database); + query.prepare( + "INSERT INTO " + kSeratoLibraryTable + " (" + + LIBRARYTABLE_TITLE + ", " + + LIBRARYTABLE_ARTIST + ", " + + LIBRARYTABLE_ALBUM + ", " + + LIBRARYTABLE_GENRE + ", " + + LIBRARYTABLE_COMMENT + ", " + + LIBRARYTABLE_GROUPING + ", " + + LIBRARYTABLE_YEAR + ", " + + LIBRARYTABLE_DURATION + ", " + + LIBRARYTABLE_BITRATE + ", " + + LIBRARYTABLE_SAMPLERATE + ", " + + LIBRARYTABLE_BPM + ", " + + LIBRARYTABLE_KEY + ", " + + LIBRARYTABLE_LOCATION + ", " + + LIBRARYTABLE_BPM_LOCK + ", " + + LIBRARYTABLE_DATETIMEADDED + ", " + "label, " + "serato_db" + ") VALUES (" + ":title, " + ":artist, " + ":album, " + ":genre, " + ":comment, " + ":grouping, " + ":year, " + ":duration, " + ":bitrate, " + ":samplerate, " + ":bpm, " + ":key, " + ":location, " + ":bpm_lock, " + ":datetime_added, " + ":label, " + ":serato_db" + ")"); + QFile databaseFile = QFile(databaseFilePath); if (!databaseFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file " @@ -267,6 +362,14 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat return QString(); } + int playlistId = createPlaylist(database, databaseDir.path(), databaseDir.path()); + if (playlistId < 0) { + qWarning() << "Failed to create library playlist for " + << databaseFilePath; + return QString(); + } + + int trackCount = 0; QByteArray headerData = databaseFile.read(8); while (headerData.length() == 8) { QString fieldName = QString(headerData.mid(0, 4)); @@ -287,12 +390,11 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } // Parse field data - qWarning() << "FieldId: " << fieldId; switch (static_cast(fieldId)) { case FieldId::Version: { QString version = parseText(data, fieldSize); - qWarning() << "Serato Database Version: " - << version; + qDebug() << "Serato Database Version: " + << version; break; } case FieldId::Track: { @@ -300,8 +402,33 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QBuffer buffer = QBuffer(&data); buffer.open(QIODevice::ReadOnly); if (parseTrack(track, buffer)) { - qWarning() << "Track: " << track.location; - // TODO + QString location = databaseRootDir.absoluteFilePath(track.location); + query.bindValue(":title", track.title); + query.bindValue(":artist", track.artist); + query.bindValue(":album", track.album); + query.bindValue(":genre", track.genre); + query.bindValue(":comment", track.comment); + query.bindValue(":grouping", track.grouping); + query.bindValue(":year", track.year); + query.bindValue(":duration", track.duration); + query.bindValue(":bitrate", track.bitrate); + query.bindValue(":samplerate", track.samplerate); + query.bindValue(":bpm", track.bpm); + query.bindValue(":key", track.key); + query.bindValue(":location", location); + query.bindValue(":bpm_lock", track.beatgridlocked); + query.bindValue(":datetime_added", track.datetimeadded); + query.bindValue(":label", track.label); + query.bindValue(":serato_db", databaseDir.path()); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + } else { + int trackId = query.lastInsertId().toInt(); + insertTrackIntoPlaylist(database, playlistId, trackId, trackCount); + trackCount++; + } + break; } break; } @@ -326,6 +453,8 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat << "."; } + transaction.commit(); + return databaseFilePath; } @@ -398,6 +527,25 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { return foundDatabases; } +void clearTable(QSqlDatabase& database, QString tableName) { + QSqlQuery query(database); + query.prepare("DELETE FROM " + tableName); + + if (!query.exec()) { + LOG_FAILED_QUERY(query) << "tableName:" << tableName; + return; + } + + query.prepare("DELETE FROM sqlite_sequence WHERE name=:name"); + query.bindValue(":name", tableName); + if (!query.exec()) { + LOG_FAILED_QUERY(query) << "tableName:" << tableName; + return; + } + + qDebug() << "Serato table entries of '" << tableName << "' have been cleared."; +} + } // anonymous namespace SeratoPlaylistModel::SeratoPlaylistModel(QObject* parent, @@ -454,6 +602,7 @@ TrackPointer SeratoPlaylistModel::getTrack(const QModelIndex& index) const { bool SeratoPlaylistModel::isColumnHiddenByDefault(int column) { if ( column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) || column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID)) { return true; } @@ -465,10 +614,59 @@ SeratoFeature::SeratoFeature( UserSettingsPointer pConfig) : BaseExternalLibraryFeature(pLibrary, pConfig), m_icon(":/images/library/ic_library_serato.svg") { + QStringList columns; + columns << LIBRARYTABLE_ID + << LIBRARYTABLE_TITLE + << LIBRARYTABLE_ARTIST + << LIBRARYTABLE_ALBUM + << LIBRARYTABLE_GENRE + << LIBRARYTABLE_COMMENT + << LIBRARYTABLE_GROUPING + << LIBRARYTABLE_YEAR + << LIBRARYTABLE_DURATION + << LIBRARYTABLE_BITRATE + << LIBRARYTABLE_SAMPLERATE + << LIBRARYTABLE_BPM + << LIBRARYTABLE_KEY + << LIBRARYTABLE_TRACKNUMBER + << LIBRARYTABLE_LOCATION + << LIBRARYTABLE_BPM_LOCK + << "label" + << "serato_db"; + + QStringList searchColumns; + searchColumns + << LIBRARYTABLE_ARTIST + << LIBRARYTABLE_TITLE + << LIBRARYTABLE_ALBUM + << LIBRARYTABLE_YEAR + << LIBRARYTABLE_GENRE + << LIBRARYTABLE_TRACKNUMBER + << LIBRARYTABLE_LOCATION + << LIBRARYTABLE_COMMENT + << LIBRARYTABLE_DURATION + << LIBRARYTABLE_BITRATE + << LIBRARYTABLE_BPM + << LIBRARYTABLE_KEY; + + m_trackSource = QSharedPointer( + new BaseTrackCache(m_pTrackCollection, kSeratoLibraryTable, LIBRARYTABLE_ID, columns, false)); + m_trackSource->setSearchColumns(searchColumns); + m_pSeratoPlaylistModel = new SeratoPlaylistModel(this, pLibrary->trackCollections(), m_trackSource); + m_title = tr("Serato"); + //Clear any previous Serato database entries if they exist + QSqlDatabase database = m_pTrackCollection->database(); + ScopedTransaction transaction(database); + clearTable(database, kSeratoPlaylistTracksTable); + clearTable(database, kSeratoPlaylistsTable); + clearTable(database, kSeratoLibraryTable); + transaction.commit(); + connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, this, &SeratoFeature::onSeratoDatabasesFound); connect(&m_tracksFutureWatcher, &QFutureWatcher::finished, this, &SeratoFeature::onTracksFound); + // initialize the model m_childModel.setRootItem(std::make_unique(this)); } @@ -550,7 +748,7 @@ void SeratoFeature::refreshLibraryModels() { void SeratoFeature::activate() { qDebug() << "SeratoFeature::activate()"; - // Let a worker thread do the XML parsing + // Let a worker thread do the parsing m_databasesFuture = QtConcurrent::run(findSeratoDatabases, this); m_databasesFutureWatcher.setFuture(m_databasesFuture); m_title = tr("(loading) Serato"); @@ -571,23 +769,12 @@ void SeratoFeature::activateChild(const QModelIndex& index) { return; } - // TreeItem list data holds 2 values in a QList and have different meanings. - // If the 2nd QList element IS_RECORDBOX_DEVICE, the 1st element is the - // filesystem device path, and the parseDeviceDB concurrent thread to parse - // the Rekcordbox database is initiated. If the 2nd element is - // IS_NOT_RECORDBOX_DEVICE, the 1st element is the playlist path and it is - // activated. - QList data = item->getData().toList(); - qDebug() << "SeratoFeature::activateChild " << item->getLabel(); - // Let a worker thread do the XML parsing + // Let a worker thread do the parsing m_tracksFuture = QtConcurrent::run(parseDatabase, static_cast(parent())->dbConnectionPool(), item); m_tracksFutureWatcher.setFuture(m_tracksFuture); - // This device is now a playlist element, future activations should treat is - // as such - //item->setData(QVariant(data)); } void SeratoFeature::onSeratoDatabasesFound() { @@ -659,4 +846,11 @@ void SeratoFeature::onSeratoDatabasesFound() { void SeratoFeature::onTracksFound() { qDebug() << "onTracksFound"; m_childModel.triggerRepaint(); + + QString databasePlaylist = m_tracksFuture.result(); + + qDebug() << "Show Serato Database Playlist: " << databasePlaylist; + + m_pSeratoPlaylistModel->setPlaylist(databasePlaylist); + emit showTrackModel(m_pSeratoPlaylistModel); } From 3e9113213f1b2c0737911a2582d1ee6f0c1dd0a0 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 1 Feb 2020 17:35:44 +0100 Subject: [PATCH 04/31] library/serato/seratofeature: Add support for parsing crates --- src/library/serato/seratofeature.cpp | 196 ++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 5 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 4def1bcea25..d98a597142b 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -271,9 +271,146 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { return true; } +inline QString parseCrateTrack(QIODevice& buffer) { + QString location; + QByteArray headerData = buffer.read(8); + while (headerData.length() == 8) { + QString fieldName = QString(headerData.mid(0, 4)); + quint32 fieldId = parseUInt32(headerData.mid(0, 4)); + quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + + // Read field data + QByteArray data = buffer.read(fieldSize); + if (static_cast(data.length()) != fieldSize) { + qWarning() << "Failed to read " + << fieldSize + << " bytes for " + << fieldName + << " field."; + return QString(); + } + + // Parse field data + switch (static_cast(fieldId)) { + case FieldId::TrackPath: + location = parseText(data, fieldSize); + break; + default: + qDebug() << "Ignoring unknown field " + << fieldName + << " (" + << fieldSize + << " bytes)."; + } + + headerData = buffer.read(8); + } + + if (headerData.length() != 0) { + qWarning() << "Found " + << headerData.length() + << " extra bytes at end of track definition."; + return QString(); + } + + return location; +} + +QString parseCrate(QSqlDatabase& database, QString databasePath, QString crateFilePath, const QMap& trackIdMap) { + QString crateName = QFileInfo(crateFilePath).baseName(); + qDebug() << "Parsing crate" + << crateName + << "at" << crateFilePath; + + //Open the database connection in this thread. + VERIFY_OR_DEBUG_ASSERT(database.isOpen()) { + qWarning() << "Failed to open database for Serato parser." + << database.lastError(); + return QString(); + } + + QFile crateFile = QFile(crateFilePath); + if (!crateFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file " + << crateFilePath + << " for reading."; + return QString(); + } + + int playlistId = createPlaylist(database, crateFilePath, databasePath); + if (playlistId < 0) { + qWarning() << "Failed to create library playlist for " + << crateFilePath; + return QString(); + } + + int trackCount = 0; + QByteArray headerData = crateFile.read(8); + while (headerData.length() == 8) { + QString fieldName = QString(headerData.mid(0, 4)); + quint32 fieldId = parseUInt32(headerData.mid(0, 4)); + quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + + // Read field data + QByteArray data = crateFile.read(fieldSize); + if (static_cast(data.length()) != fieldSize) { + qWarning() << "Failed to read " + << fieldSize + << " bytes for " + << fieldName + << " field from " + << crateFilePath + << "."; + return QString(); + } + + // Parse field data + switch (static_cast(fieldId)) { + case FieldId::Version: { + QString version = parseText(data, fieldSize); + qDebug() << "Serato Database Version: " + << version; + break; + } + case FieldId::Track: { + QBuffer buffer = QBuffer(&data); + buffer.open(QIODevice::ReadOnly); + QString location = parseCrateTrack(buffer); + if (!location.isEmpty()) { + int trackId = trackIdMap.value(location, -1); + insertTrackIntoPlaylist(database, playlistId, trackId, trackCount); + trackCount++; + break; + } + break; + } + default: + qDebug() << "Ignoring unknown field " + << fieldName + << " (" + << fieldSize + << " bytes) in database " + << crateFilePath + << "."; + } + + headerData = crateFile.read(8); + } + + if (headerData.length() != 0) { + qWarning() << "Found " + << headerData.length() + << " extra bytes at end of Serato database file " + << crateFilePath + << "."; + } + + return crateName; +} + QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* databaseItem) { QString databaseName = databaseItem->getLabel(); - QDir databaseDir = QDir(databaseItem->getData().toString()); + QDir databaseDir = QDir(databaseItem->getData().toList()[0].toString()); QString databaseFilePath = databaseDir.filePath(kDatabaseFilename); QDir databaseRootDir = QDir(databaseDir); @@ -370,6 +507,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } int trackCount = 0; + QMap trackIdMap; QByteArray headerData = databaseFile.read(8); while (headerData.length() == 8) { QString fieldName = QString(headerData.mid(0, 4)); @@ -426,6 +564,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } else { int trackId = query.lastInsertId().toInt(); insertTrackIntoPlaylist(database, playlistId, trackId, trackCount); + trackIdMap.insert(track.location, trackId); trackCount++; } break; @@ -453,6 +592,28 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat << "."; } + // Parse Crates + QDir crateDir = QDir(databaseDir); + if (crateDir.cd(kCrateDirectory)) { + QStringList filters; + filters << kCrateFilter; + foreach(const QString& entry, crateDir.entryList(filters)) { + QString crateFilePath = crateDir.filePath(entry); + QString crateName = parseCrate(database, databaseDir.path(), crateFilePath, trackIdMap); + if (!crateName.isEmpty()) { + QList data; + data << QVariant(crateFilePath) + << QVariant(true); + databaseItem->appendChild(crateName, data); + } + } + } else { + qWarning() << "Failed to open crate directory: " + << databaseDir.filePath(kCrateDirectory); + } + + // TODO: Parse Smart Crates + transaction.commit(); return databaseFilePath; @@ -519,7 +680,11 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { } foundDatabase->setLabel(displayPath); - foundDatabase->setData(QVariant(databaseDir.path())); + + QList data; + data << QVariant(databaseDir.path()) + << QVariant(false); + foundDatabase->setData(data); foundDatabases << foundDatabase; } @@ -769,12 +934,33 @@ void SeratoFeature::activateChild(const QModelIndex& index) { return; } + // TreeItem list data holds 2 values in a QList: + // + // 1. Playlist Name/Path (QString) + // 2. isPlaylist (boolean) + // + // If the second element is false, then the database does still have to be + // parsed. + QList data = item->getData().toList(); + QString playlist = data[0].toString(); + bool isPlaylist = data[1].toBool(); + qDebug() << "SeratoFeature::activateChild " << item->getLabel(); - // Let a worker thread do the parsing - m_tracksFuture = QtConcurrent::run(parseDatabase, static_cast(parent())->dbConnectionPool(), item); - m_tracksFutureWatcher.setFuture(m_tracksFuture); + if (!isPlaylist) { + // Let a worker thread do the parsing + m_tracksFuture = QtConcurrent::run(parseDatabase, static_cast(parent())->dbConnectionPool(), item); + m_tracksFutureWatcher.setFuture(m_tracksFuture); + // This device is now a playlist element, future activations should + // treat is as such + data[1] = QVariant(true); + item->setData(QVariant(data)); + } else { + qDebug() << "Activate Serato Playlist: " << playlist; + m_pSeratoPlaylistModel->setPlaylist(playlist); + emit showTrackModel(m_pSeratoPlaylistModel); + } } void SeratoFeature::onSeratoDatabasesFound() { From 9b56230ff572685dffd820a57c640dfd64087a46 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 1 Feb 2020 18:54:09 +0100 Subject: [PATCH 05/31] library/serato/seratofeature: Fix root view HTML text --- src/library/serato/seratofeature.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index d98a597142b..be98bc5f074 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -884,10 +884,11 @@ TreeItemModel* SeratoFeature::getChildModel() { QString SeratoFeature::formatRootViewHtml() const { QString title = tr("Serato"); - QString summary = tr("Reads the following from Serato the Music directory and removable devices:"); + QString summary = tr("Reads the following from the Serato Music directory and removable devices:"); QStringList items; - items << tr("Absolutely nothing yet :("); + items << tr("Tracks") + << tr("Crates"); QString html; QString refreshLink = tr("Check for Serato databases (refresh)"); From f9f469fe6515b01118eae0f7a410df8bb6c198fb Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 2 Feb 2020 01:43:17 +0100 Subject: [PATCH 06/31] library/serato/seratofeature: Use QStringLiteral for QString constants --- src/library/serato/seratofeature.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index be98bc5f074..433d58825d4 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -89,16 +89,16 @@ struct serato_track_t { quint32 datetimeadded = 0; }; -const QString kDatabaseDirectory = "_Serato_"; -const QString kDatabaseFilename = "database V2"; -const QString kCrateDirectory = "Subcrates"; -const QString kCrateFilter = "*.crate"; -const QString kSmartCrateDirectory = "Smart Crates"; -const QString kSmartCrateFilter = "*.scrate"; - -const QString kSeratoLibraryTable = "serato_library"; -const QString kSeratoPlaylistsTable = "serato_library"; -const QString kSeratoPlaylistTracksTable = "serato_library"; +const QString kDatabaseDirectory = QStringLiteral("_Serato_"); +const QString kDatabaseFilename = QStringLiteral("database V2"); +const QString kCrateDirectory = QStringLiteral("Subcrates"); +const QString kCrateFilter = QStringLiteral("*.crate"); +const QString kSmartCrateDirectory = QStringLiteral("Smart Crates"); +const QString kSmartCrateFilter = QStringLiteral("*.scrate"); + +const QString kSeratoLibraryTable = QStringLiteral("serato_library"); +const QString kSeratoPlaylistsTable = QStringLiteral("serato_library"); +const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_library"); int createPlaylist(QSqlDatabase& database, QString name, QString databasePath) { QSqlQuery query(database); From b58ca8432b4da8e6667e4491e2498b8e7b05a519 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 2 Feb 2020 01:49:42 +0100 Subject: [PATCH 07/31] library/serato/seratofeature: Fix non-const reference arguments --- src/library/serato/seratofeature.cpp | 64 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 433d58825d4..9a2259bca1a 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -100,7 +100,7 @@ const QString kSeratoLibraryTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistsTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_library"); -int createPlaylist(QSqlDatabase& database, QString name, QString databasePath) { +int createPlaylist(const QSqlDatabase& database, const QString& name, const QString& databasePath) { QSqlQuery query(database); query.prepare( "INSERT INTO serato_playlists (name, serato_db)" @@ -116,7 +116,7 @@ int createPlaylist(QSqlDatabase& database, QString name, QString databasePath) { return query.lastInsertId().toInt(); } -int insertTrackIntoPlaylist(QSqlDatabase& database, int playlistId, int trackId, int position) { +int insertTrackIntoPlaylist(const QSqlDatabase& database, int playlistId, int trackId, int position) { QSqlQuery query(database); query.prepare( "INSERT INTO serato_playlist_tracks (playlist_id, track_id, position) " @@ -150,15 +150,15 @@ inline quint32 parseUInt32(const QByteArray& data) { #endif } -inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { - QByteArray headerData = buffer.read(8); +inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { + QByteArray headerData = buffer->read(8); while (headerData.length() == 8) { QString fieldName = QString(headerData.mid(0, 4)); quint32 fieldId = parseUInt32(headerData.mid(0, 4)); quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); // Read field data - QByteArray data = buffer.read(fieldSize); + QByteArray data = buffer->read(fieldSize); if (static_cast(data.length()) != fieldSize) { qWarning() << "Failed to read " << fieldSize @@ -171,76 +171,76 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { // Parse field data switch (static_cast(fieldId)) { case FieldId::FileType: - track.filetype = parseText(data, fieldSize); + track->filetype = parseText(data, fieldSize); break; case FieldId::FilePath: - track.location = parseText(data, fieldSize); + track->location = parseText(data, fieldSize); break; case FieldId::SongTitle: - track.title = parseText(data, fieldSize); + track->title = parseText(data, fieldSize); break; case FieldId::Artist: - track.artist = parseText(data, fieldSize); + track->artist = parseText(data, fieldSize); break; case FieldId::Album: - track.album = parseText(data, fieldSize); + track->album = parseText(data, fieldSize); break; case FieldId::Genre: - track.genre = parseText(data, fieldSize); + track->genre = parseText(data, fieldSize); break; case FieldId::Length: { bool ok; int duration = parseText(data, fieldSize).toInt(&ok); if (ok) { - track.duration = duration; + track->duration = duration; } break; } case FieldId::Bitrate: - track.bitrate = parseText(data, fieldSize); + track->bitrate = parseText(data, fieldSize); break; case FieldId::SampleRate: - track.samplerate = parseText(data, fieldSize); + track->samplerate = parseText(data, fieldSize); break; case FieldId::Bpm: { bool ok; double bpm = parseText(data, fieldSize).toDouble(&ok); if (ok) { - track.bpm = bpm; + track->bpm = bpm; } break; } case FieldId::Comment: - track.comment = parseText(data, fieldSize); + track->comment = parseText(data, fieldSize); break; case FieldId::Grouping: - track.grouping = parseText(data, fieldSize); + track->grouping = parseText(data, fieldSize); break; case FieldId::Label: - track.label = parseText(data, fieldSize); + track->label = parseText(data, fieldSize); break; case FieldId::Year: { bool ok; int year = parseText(data, fieldSize).toInt(&ok); if (ok) { - track.year = year; + track->year = year; } break; } case FieldId::Key: - track.key = parseText(data, fieldSize); + track->key = parseText(data, fieldSize); break; case FieldId::BeatgridLocked: - track.beatgridlocked = parseBoolean(data); + track->beatgridlocked = parseBoolean(data); break; case FieldId::Missing: - track.missing = parseBoolean(data); + track->missing = parseBoolean(data); break; case FieldId::FileTime: - track.filetime = parseUInt32(data); + track->filetime = parseUInt32(data); break; case FieldId::DateAdded: - track.datetimeadded = parseUInt32(data); + track->datetimeadded = parseUInt32(data); break; case FieldId::DateAddedText: // Ignore this field, but do not print a debug message @@ -253,7 +253,7 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { << " bytes)."; } - headerData = buffer.read(8); + headerData = buffer->read(8); } if (headerData.length() != 0) { @@ -263,7 +263,7 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { return false; } - if (track.location.isEmpty()) { + if (track->location.isEmpty()) { qWarning() << "Found track with empty location field."; return false; } @@ -271,16 +271,16 @@ inline bool parseTrack(serato_track_t& track, QIODevice& buffer) { return true; } -inline QString parseCrateTrack(QIODevice& buffer) { +inline QString parseCrateTrack(QIODevice* buffer) { QString location; - QByteArray headerData = buffer.read(8); + QByteArray headerData = buffer->read(8); while (headerData.length() == 8) { QString fieldName = QString(headerData.mid(0, 4)); quint32 fieldId = parseUInt32(headerData.mid(0, 4)); quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); // Read field data - QByteArray data = buffer.read(fieldSize); + QByteArray data = buffer->read(fieldSize); if (static_cast(data.length()) != fieldSize) { qWarning() << "Failed to read " << fieldSize @@ -303,7 +303,7 @@ inline QString parseCrateTrack(QIODevice& buffer) { << " bytes)."; } - headerData = buffer.read(8); + headerData = buffer->read(8); } if (headerData.length() != 0) { @@ -375,7 +375,7 @@ QString parseCrate(QSqlDatabase& database, QString databasePath, QString crateFi case FieldId::Track: { QBuffer buffer = QBuffer(&data); buffer.open(QIODevice::ReadOnly); - QString location = parseCrateTrack(buffer); + QString location = parseCrateTrack(&buffer); if (!location.isEmpty()) { int trackId = trackIdMap.value(location, -1); insertTrackIntoPlaylist(database, playlistId, trackId, trackCount); @@ -539,7 +539,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat serato_track_t track; QBuffer buffer = QBuffer(&data); buffer.open(QIODevice::ReadOnly); - if (parseTrack(track, buffer)) { + if (parseTrack(&track, &buffer)) { QString location = databaseRootDir.absoluteFilePath(track.location); query.bindValue(":title", track.title); query.bindValue(":artist", track.artist); From 89be82227971ba41c602511d3f1a8e15c760fd99 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 2 Feb 2020 01:51:20 +0100 Subject: [PATCH 08/31] library/serato/seratofeature: Add kHeaderSize constexpr --- src/library/serato/seratofeature.cpp | 50 +++++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 9a2259bca1a..7cffdc5e5a0 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -100,6 +100,8 @@ const QString kSeratoLibraryTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistsTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_library"); +constexpr int kHeaderSize = 2*sizeof(quint32); + int createPlaylist(const QSqlDatabase& database, const QString& name, const QString& databasePath) { QSqlQuery query(database); query.prepare( @@ -151,11 +153,11 @@ inline quint32 parseUInt32(const QByteArray& data) { } inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { - QByteArray headerData = buffer->read(8); - while (headerData.length() == 8) { - QString fieldName = QString(headerData.mid(0, 4)); - quint32 fieldId = parseUInt32(headerData.mid(0, 4)); - quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + QByteArray headerData = buffer->read(kHeaderSize); + while (headerData.length() == kHeaderSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); + quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); @@ -253,7 +255,7 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { << " bytes)."; } - headerData = buffer->read(8); + headerData = buffer->read(kHeaderSize); } if (headerData.length() != 0) { @@ -273,11 +275,11 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { inline QString parseCrateTrack(QIODevice* buffer) { QString location; - QByteArray headerData = buffer->read(8); - while (headerData.length() == 8) { - QString fieldName = QString(headerData.mid(0, 4)); - quint32 fieldId = parseUInt32(headerData.mid(0, 4)); - quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + QByteArray headerData = buffer->read(kHeaderSize); + while (headerData.length() == kHeaderSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); + quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); @@ -303,7 +305,7 @@ inline QString parseCrateTrack(QIODevice* buffer) { << " bytes)."; } - headerData = buffer->read(8); + headerData = buffer->read(kHeaderSize); } if (headerData.length() != 0) { @@ -345,11 +347,11 @@ QString parseCrate(QSqlDatabase& database, QString databasePath, QString crateFi } int trackCount = 0; - QByteArray headerData = crateFile.read(8); - while (headerData.length() == 8) { - QString fieldName = QString(headerData.mid(0, 4)); - quint32 fieldId = parseUInt32(headerData.mid(0, 4)); - quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + QByteArray headerData = crateFile.read(kHeaderSize); + while (headerData.length() == kHeaderSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); + quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = crateFile.read(fieldSize); @@ -394,7 +396,7 @@ QString parseCrate(QSqlDatabase& database, QString databasePath, QString crateFi << "."; } - headerData = crateFile.read(8); + headerData = crateFile.read(kHeaderSize); } if (headerData.length() != 0) { @@ -508,11 +510,11 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat int trackCount = 0; QMap trackIdMap; - QByteArray headerData = databaseFile.read(8); - while (headerData.length() == 8) { - QString fieldName = QString(headerData.mid(0, 4)); - quint32 fieldId = parseUInt32(headerData.mid(0, 4)); - quint32 fieldSize = parseUInt32(headerData.mid(4, 8)); + QByteArray headerData = databaseFile.read(kHeaderSize); + while (headerData.length() == kHeaderSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); + quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = databaseFile.read(fieldSize); @@ -581,7 +583,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat << "."; } - headerData = databaseFile.read(8); + headerData = databaseFile.read(kHeaderSize); } if (headerData.length() != 0) { From dba8e03ee73b69516f2f10c8c16713d2d2dd4ea1 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 2 Feb 2020 01:52:31 +0100 Subject: [PATCH 09/31] library/serato/seratofeature: Add some more length checks and comments --- src/library/serato/seratofeature.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 7cffdc5e5a0..12415a77d6e 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -21,6 +21,7 @@ #include "track/beatfactory.h" #include "track/cue.h" #include "track/keyfactory.h" +#include "util/assert.h" #include "util/color/color.h" #include "util/db/dbconnectionpooled.h" #include "util/db/dbconnectionpooler.h" @@ -140,10 +141,16 @@ inline QString parseText(const QByteArray& data, const quint32 size) { } inline bool parseBoolean(const QByteArray& data) { + VERIFY_OR_DEBUG_ASSERT(!data.isEmpty()) { + return false; + } return data.at(0) != 0; } inline quint32 parseUInt32(const QByteArray& data) { + VERIFY_OR_DEBUG_ASSERT(data.size() >= static_cast(sizeof(quint32))) { + return 0; + } #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) return qFromBigEndian(data); #else @@ -222,6 +229,7 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { track->label = parseText(data, fieldSize); break; case FieldId::Year: { + // 4-digit year as string (YYYY) bool ok; int year = parseText(data, fieldSize).toInt(&ok); if (ok) { @@ -236,16 +244,27 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { track->beatgridlocked = parseBoolean(data); break; case FieldId::Missing: - track->missing = parseBoolean(data); + if (fieldSize == 1) { + track->missing = parseBoolean(data); + } break; case FieldId::FileTime: - track->filetime = parseUInt32(data); + // POSIX timestamp + if (fieldSize == sizeof(quint32)) { + track->filetime = parseUInt32(data); + } break; case FieldId::DateAdded: - track->datetimeadded = parseUInt32(data); + // POSIX timestamp + if (fieldSize == sizeof(quint32)) { + track->datetimeadded = parseUInt32(data); + } break; case FieldId::DateAddedText: - // Ignore this field, but do not print a debug message + // Ignore this field, but do not print a debug message. It's the + // same as the regular DateAdded field, but this time the timestamp + // is a string instead of an unsigned integer. Since we already + // parse the integer version, it doesn't make sense to parse this. break; default: qDebug() << "Ignoring unknown field " From dcfb3532c1aa59fd870c8aa46cfbea24a5e3adb1 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 2 Feb 2020 01:59:39 +0100 Subject: [PATCH 10/31] library/serato/seratofeature: Use const reference as parseCrate param --- src/library/serato/seratofeature.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 12415a77d6e..86002862cd4 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -337,7 +337,8 @@ inline QString parseCrateTrack(QIODevice* buffer) { return location; } -QString parseCrate(QSqlDatabase& database, QString databasePath, QString crateFilePath, const QMap& trackIdMap) { +QString parseCrate(const QSqlDatabase& database, QString databasePath, + QString crateFilePath, const QMap& trackIdMap) { QString crateName = QFileInfo(crateFilePath).baseName(); qDebug() << "Parsing crate" << crateName From 8e7b51249885f6fbdb0504a25b260dd7d92d6c1f Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:28:21 +0100 Subject: [PATCH 11/31] library/serato/seratofeature: Use const references for parseCrate() --- src/library/serato/seratofeature.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 86002862cd4..8e9623e1997 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -337,8 +337,8 @@ inline QString parseCrateTrack(QIODevice* buffer) { return location; } -QString parseCrate(const QSqlDatabase& database, QString databasePath, - QString crateFilePath, const QMap& trackIdMap) { +QString parseCrate(const QSqlDatabase& database, const QString& databasePath, + const QString& crateFilePath, const QMap& trackIdMap) { QString crateName = QFileInfo(crateFilePath).baseName(); qDebug() << "Parsing crate" << crateName From 66aec2521743eee0bec7537c41da97652d627720 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:29:40 +0100 Subject: [PATCH 12/31] library/serato/seratofeature: Rename func to parseCrateTrackPath() --- src/library/serato/seratofeature.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 8e9623e1997..25b08f42024 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -292,7 +292,7 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { return true; } -inline QString parseCrateTrack(QIODevice* buffer) { +inline QString parseCrateTrackPath(QIODevice* buffer) { QString location; QByteArray headerData = buffer->read(kHeaderSize); while (headerData.length() == kHeaderSize) { @@ -397,7 +397,7 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, case FieldId::Track: { QBuffer buffer = QBuffer(&data); buffer.open(QIODevice::ReadOnly); - QString location = parseCrateTrack(&buffer); + QString location = parseCrateTrackPath(&buffer); if (!location.isEmpty()) { int trackId = trackIdMap.value(location, -1); insertTrackIntoPlaylist(database, playlistId, trackId, trackCount); From 3e9a16ff0fba6d23b494ac6f2bb24b79e5fd17f4 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:31:06 +0100 Subject: [PATCH 13/31] library/serato/seratofeature: Improve findSeratoDatabases() --- src/library/serato/seratofeature.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 25b08f42024..410e7e5ef04 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -667,21 +667,29 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { #elif defined(__LINUX__) // To get devices on Linux, we look for directories under /media and // /run/media/$USER. + const QString userName = QString::fromLocal8Bit(qgetenv("USER")); // Add folders under /media to devices. - databaseLocations += QDir(QStringLiteral("/media")).entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot); + QDir mediaDir = QDir(QStringLiteral("/media/")); + databaseLocations.append( + mediaDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); // Add folders under /media/$USER to devices. - QDir mediaUserDir(QStringLiteral("/media/") + QString::fromLocal8Bit(qgetenv("USER"))); - databaseLocations += mediaUserDir.entryInfoList( - QDir::AllDirs | QDir::NoDotAndDotDot); + if (mediaDir.cd(userName)) { + databaseLocations.append( + mediaDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); + } // Add folders under /run/media/$USER to devices. - QDir runMediaUserDir(QStringLiteral("/run/media/") + QString::fromLocal8Bit(qgetenv("USER"))); - databaseLocations += runMediaUserDir.entryInfoList( - QDir::AllDirs | QDir::NoDotAndDotDot); + QDir runMediaDir = QDir(QStringLiteral("/run/media/")); + if (runMediaDir.cd(userName)) { + databaseLocations.append( + runMediaDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); + } #elif defined(__APPLE__) - databaseLocations.append(QDir(QStringLiteral("/Volumes")).entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); + QDir volumesDir = QDir(QStringLiteral("/Volumes")); + databaseLocations.append( + volumesDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); #endif foreach (QFileInfo databaseLocation, databaseLocations) { From 4d65886dead94baceb8f2e047dbb328907b8843e Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:37:35 +0100 Subject: [PATCH 14/31] library/serato/seratofeature: Add comment regarding track location usage --- src/library/serato/seratofeature.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 410e7e5ef04..db110a6a16a 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -284,6 +284,8 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { return false; } + // Ignore tracks with empty location fields. The track location is used as + // identifier by Serato (e.g. it's also used to reference them in Crates). if (track->location.isEmpty()) { qWarning() << "Found track with empty location field."; return false; From 55bb572cf8e2a886375ac0ca0c92a494a074f7f6 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:56:21 +0100 Subject: [PATCH 15/31] library/serato/seratofeature: Rename byte conversion functions --- src/library/serato/seratofeature.cpp | 68 ++++++++++++++-------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index db110a6a16a..d30439245b5 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -136,23 +136,23 @@ int insertTrackIntoPlaylist(const QSqlDatabase& database, int playlistId, int tr return query.lastInsertId().toInt(); } -inline QString parseText(const QByteArray& data, const quint32 size) { +inline QString bytesToQString(const QByteArray& data, const quint32 size) { return QTextCodec::codecForName("UTF-16BE")->toUnicode(data, size); } -inline bool parseBoolean(const QByteArray& data) { +inline bool bytesToBoolean(const QByteArray& data) { VERIFY_OR_DEBUG_ASSERT(!data.isEmpty()) { return false; } return data.at(0) != 0; } -inline quint32 parseUInt32(const QByteArray& data) { +inline quint32 bytesToUInt32(const QByteArray& data) { VERIFY_OR_DEBUG_ASSERT(data.size() >= static_cast(sizeof(quint32))) { return 0; } #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) - return qFromBigEndian(data); + return qFromBigEndian(data.constData()); #else return qFromBigEndian( reinterpret_cast(data.constData())); @@ -163,8 +163,8 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { QByteArray headerData = buffer->read(kHeaderSize); while (headerData.length() == kHeaderSize) { QString fieldName = QString(headerData.mid(0, sizeof(quint32))); - quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); - quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); + quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); @@ -180,84 +180,84 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { // Parse field data switch (static_cast(fieldId)) { case FieldId::FileType: - track->filetype = parseText(data, fieldSize); + track->filetype = bytesToQString(data, fieldSize); break; case FieldId::FilePath: - track->location = parseText(data, fieldSize); + track->location = bytesToQString(data, fieldSize); break; case FieldId::SongTitle: - track->title = parseText(data, fieldSize); + track->title = bytesToQString(data, fieldSize); break; case FieldId::Artist: - track->artist = parseText(data, fieldSize); + track->artist = bytesToQString(data, fieldSize); break; case FieldId::Album: - track->album = parseText(data, fieldSize); + track->album = bytesToQString(data, fieldSize); break; case FieldId::Genre: - track->genre = parseText(data, fieldSize); + track->genre = bytesToQString(data, fieldSize); break; case FieldId::Length: { bool ok; - int duration = parseText(data, fieldSize).toInt(&ok); + int duration = bytesToQString(data, fieldSize).toInt(&ok); if (ok) { track->duration = duration; } break; } case FieldId::Bitrate: - track->bitrate = parseText(data, fieldSize); + track->bitrate = bytesToQString(data, fieldSize); break; case FieldId::SampleRate: - track->samplerate = parseText(data, fieldSize); + track->samplerate = bytesToQString(data, fieldSize); break; case FieldId::Bpm: { bool ok; - double bpm = parseText(data, fieldSize).toDouble(&ok); + double bpm = bytesToQString(data, fieldSize).toDouble(&ok); if (ok) { track->bpm = bpm; } break; } case FieldId::Comment: - track->comment = parseText(data, fieldSize); + track->comment = bytesToQString(data, fieldSize); break; case FieldId::Grouping: - track->grouping = parseText(data, fieldSize); + track->grouping = bytesToQString(data, fieldSize); break; case FieldId::Label: - track->label = parseText(data, fieldSize); + track->label = bytesToQString(data, fieldSize); break; case FieldId::Year: { // 4-digit year as string (YYYY) bool ok; - int year = parseText(data, fieldSize).toInt(&ok); + int year = bytesToQString(data, fieldSize).toInt(&ok); if (ok) { track->year = year; } break; } case FieldId::Key: - track->key = parseText(data, fieldSize); + track->key = bytesToQString(data, fieldSize); break; case FieldId::BeatgridLocked: - track->beatgridlocked = parseBoolean(data); + track->beatgridlocked = bytesToBoolean(data); break; case FieldId::Missing: if (fieldSize == 1) { - track->missing = parseBoolean(data); + track->missing = bytesToBoolean(data); } break; case FieldId::FileTime: // POSIX timestamp if (fieldSize == sizeof(quint32)) { - track->filetime = parseUInt32(data); + track->filetime = bytesToUInt32(data); } break; case FieldId::DateAdded: // POSIX timestamp if (fieldSize == sizeof(quint32)) { - track->datetimeadded = parseUInt32(data); + track->datetimeadded = bytesToUInt32(data); } break; case FieldId::DateAddedText: @@ -299,8 +299,8 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { QByteArray headerData = buffer->read(kHeaderSize); while (headerData.length() == kHeaderSize) { QString fieldName = QString(headerData.mid(0, sizeof(quint32))); - quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); - quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); + quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); @@ -316,7 +316,7 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { // Parse field data switch (static_cast(fieldId)) { case FieldId::TrackPath: - location = parseText(data, fieldSize); + location = bytesToQString(data, fieldSize); break; default: qDebug() << "Ignoring unknown field " @@ -372,8 +372,8 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, QByteArray headerData = crateFile.read(kHeaderSize); while (headerData.length() == kHeaderSize) { QString fieldName = QString(headerData.mid(0, sizeof(quint32))); - quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); - quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); + quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = crateFile.read(fieldSize); @@ -391,7 +391,7 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, // Parse field data switch (static_cast(fieldId)) { case FieldId::Version: { - QString version = parseText(data, fieldSize); + QString version = bytesToQString(data, fieldSize); qDebug() << "Serato Database Version: " << version; break; @@ -535,8 +535,8 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QByteArray headerData = databaseFile.read(kHeaderSize); while (headerData.length() == kHeaderSize) { QString fieldName = QString(headerData.mid(0, sizeof(quint32))); - quint32 fieldId = parseUInt32(headerData.mid(0, sizeof(quint32))); - quint32 fieldSize = parseUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); + quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); + quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = databaseFile.read(fieldSize); @@ -554,7 +554,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat // Parse field data switch (static_cast(fieldId)) { case FieldId::Version: { - QString version = parseText(data, fieldSize); + QString version = bytesToQString(data, fieldSize); qDebug() << "Serato Database Version: " << version; break; From e6e006b35dbe394442a2db6c590a449cd80a948e Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 4 Feb 2020 23:57:36 +0100 Subject: [PATCH 16/31] library/serato/seratofeature: Break long lines --- src/library/serato/seratofeature.cpp | 75 ++++++++++++++++++---------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index d30439245b5..21b5ced2337 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -757,29 +757,52 @@ void SeratoPlaylistModel::initSortColumnMapping() { m_columnIndexBySortColumnId[i] = -1; } - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] + = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] + = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); m_sortColumnIdByColumnIndex.clear(); for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { @@ -861,8 +884,10 @@ SeratoFeature::SeratoFeature( clearTable(database, kSeratoLibraryTable); transaction.commit(); - connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, this, &SeratoFeature::onSeratoDatabasesFound); - connect(&m_tracksFutureWatcher, &QFutureWatcher::finished, this, &SeratoFeature::onTracksFound); + connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, + this, &SeratoFeature::onSeratoDatabasesFound); + connect(&m_tracksFutureWatcher, &QFutureWatcher::finished, + this, &SeratoFeature::onTracksFound); // initialize the model m_childModel.setRootItem(std::make_unique(this)); From 7a68125842391c0fe57decf18efdeef2573e5eb9 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 00:03:39 +0100 Subject: [PATCH 17/31] library/serato/seratofeature: Only read field names when necessary --- src/library/serato/seratofeature.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 21b5ced2337..8b439685b81 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -162,13 +162,13 @@ inline quint32 bytesToUInt32(const QByteArray& data) { inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { QByteArray headerData = buffer->read(kHeaderSize); while (headerData.length() == kHeaderSize) { - QString fieldName = QString(headerData.mid(0, sizeof(quint32))); quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); if (static_cast(data.length()) != fieldSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qWarning() << "Failed to read " << fieldSize << " bytes for " @@ -266,13 +266,15 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { // is a string instead of an unsigned integer. Since we already // parse the integer version, it doesn't make sense to parse this. break; - default: + default: { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qDebug() << "Ignoring unknown field " << fieldName << " (" << fieldSize << " bytes)."; } + } headerData = buffer->read(kHeaderSize); } @@ -298,13 +300,13 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { QString location; QByteArray headerData = buffer->read(kHeaderSize); while (headerData.length() == kHeaderSize) { - QString fieldName = QString(headerData.mid(0, sizeof(quint32))); quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = buffer->read(fieldSize); if (static_cast(data.length()) != fieldSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qWarning() << "Failed to read " << fieldSize << " bytes for " @@ -318,13 +320,15 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { case FieldId::TrackPath: location = bytesToQString(data, fieldSize); break; - default: + default: { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qDebug() << "Ignoring unknown field " << fieldName << " (" << fieldSize << " bytes)."; } + } headerData = buffer->read(kHeaderSize); } @@ -371,13 +375,13 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, int trackCount = 0; QByteArray headerData = crateFile.read(kHeaderSize); while (headerData.length() == kHeaderSize) { - QString fieldName = QString(headerData.mid(0, sizeof(quint32))); quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = crateFile.read(fieldSize); if (static_cast(data.length()) != fieldSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qWarning() << "Failed to read " << fieldSize << " bytes for " @@ -408,7 +412,8 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, } break; } - default: + default: { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qDebug() << "Ignoring unknown field " << fieldName << " (" @@ -417,6 +422,7 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, << crateFilePath << "."; } + } headerData = crateFile.read(kHeaderSize); } @@ -534,13 +540,13 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QMap trackIdMap; QByteArray headerData = databaseFile.read(kHeaderSize); while (headerData.length() == kHeaderSize) { - QString fieldName = QString(headerData.mid(0, sizeof(quint32))); quint32 fieldId = bytesToUInt32(headerData.mid(0, sizeof(quint32))); quint32 fieldSize = bytesToUInt32(headerData.mid(sizeof(quint32), kHeaderSize)); // Read field data QByteArray data = databaseFile.read(fieldSize); if (static_cast(data.length()) != fieldSize) { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qWarning() << "Failed to read " << fieldSize << " bytes for " @@ -595,7 +601,8 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } break; } - default: + default: { + QString fieldName = QString(headerData.mid(0, sizeof(quint32))); qDebug() << "Ignoring unknown field " << fieldName << " (" @@ -604,6 +611,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat << databaseFilePath << "."; } + } headerData = databaseFile.read(kHeaderSize); } From bba4caecf3217042838149bce08b5d73faf06d59 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 00:42:38 +0100 Subject: [PATCH 18/31] library/serato/seratofeature: Remove columns without headers from table --- src/library/serato/seratofeature.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 8b439685b81..960ca3dd443 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -858,9 +858,7 @@ SeratoFeature::SeratoFeature( << LIBRARYTABLE_KEY << LIBRARYTABLE_TRACKNUMBER << LIBRARYTABLE_LOCATION - << LIBRARYTABLE_BPM_LOCK - << "label" - << "serato_db"; + << LIBRARYTABLE_BPM_LOCK; QStringList searchColumns; searchColumns From 4509118e23a02f77b8b8225abc856406eed208f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Wed, 5 Feb 2020 21:24:40 +0100 Subject: [PATCH 19/31] apply changes from git-clang-format --- src/library/library.cpp | 6 +- src/library/serato/seratofeature.cpp | 161 ++++++++++++---------- src/preferences/dialog/dlgpreflibrary.cpp | 6 +- 3 files changed, 93 insertions(+), 80 deletions(-) diff --git a/src/library/library.cpp b/src/library/library.cpp index 3bed66ca09c..6521c4bd7b3 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -20,6 +20,7 @@ #include "library/trackcollectionmanager.h" #include "library/trackmodel.h" +#include "library/analysisfeature.h" #include "library/autodj/autodjfeature.h" #include "library/banshee/bansheefeature.h" #include "library/browse/browsefeature.h" @@ -28,12 +29,11 @@ #include "library/mixxxlibraryfeature.h" #include "library/playlistfeature.h" #include "library/recording/recordingfeature.h" +#include "library/rekordbox/rekordboxfeature.h" #include "library/rhythmbox/rhythmboxfeature.h" +#include "library/serato/seratofeature.h" #include "library/setlogfeature.h" #include "library/traktor/traktorfeature.h" -#include "library/rekordbox/rekordboxfeature.h" -#include "library/serato/seratofeature.h" -#include "library/analysisfeature.h" #include "mixer/playermanager.h" diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 960ca3dd443..a95dfd6527a 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -10,6 +10,7 @@ #include #include "engine/engine.h" +#include "library/dao/trackschema.h" #include "library/library.h" #include "library/librarytablemodel.h" #include "library/missingtablemodel.h" @@ -17,7 +18,6 @@ #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "library/treeitem.h" -#include "library/dao/trackschema.h" #include "track/beatfactory.h" #include "track/cue.h" #include "track/keyfactory.h" @@ -101,7 +101,7 @@ const QString kSeratoLibraryTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistsTable = QStringLiteral("serato_library"); const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_library"); -constexpr int kHeaderSize = 2*sizeof(quint32); +constexpr int kHeaderSize = 2 * sizeof(quint32); int createPlaylist(const QSqlDatabase& database, const QString& name, const QString& databasePath) { QSqlQuery query(database); @@ -343,12 +343,15 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { return location; } -QString parseCrate(const QSqlDatabase& database, const QString& databasePath, - const QString& crateFilePath, const QMap& trackIdMap) { +QString parseCrate( + const QSqlDatabase& database, + const QString& databasePath, + const QString& crateFilePath, + const QMap& trackIdMap) { QString crateName = QFileInfo(crateFilePath).baseName(); qDebug() << "Parsing crate" - << crateName - << "at" << crateFilePath; + << crateName + << "at" << crateFilePath; //Open the database connection in this thread. VERIFY_OR_DEBUG_ASSERT(database.isOpen()) { @@ -483,22 +486,24 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QSqlQuery query(database); query.prepare( - "INSERT INTO " + kSeratoLibraryTable + " (" - + LIBRARYTABLE_TITLE + ", " - + LIBRARYTABLE_ARTIST + ", " - + LIBRARYTABLE_ALBUM + ", " - + LIBRARYTABLE_GENRE + ", " - + LIBRARYTABLE_COMMENT + ", " - + LIBRARYTABLE_GROUPING + ", " - + LIBRARYTABLE_YEAR + ", " - + LIBRARYTABLE_DURATION + ", " - + LIBRARYTABLE_BITRATE + ", " - + LIBRARYTABLE_SAMPLERATE + ", " - + LIBRARYTABLE_BPM + ", " - + LIBRARYTABLE_KEY + ", " - + LIBRARYTABLE_LOCATION + ", " - + LIBRARYTABLE_BPM_LOCK + ", " - + LIBRARYTABLE_DATETIMEADDED + ", " + "INSERT INTO " + + kSeratoLibraryTable + " (" + + LIBRARYTABLE_TITLE + ", " + + LIBRARYTABLE_ARTIST + ", " + + LIBRARYTABLE_ALBUM + ", " + + LIBRARYTABLE_GENRE + ", " + + LIBRARYTABLE_COMMENT + ", " + + LIBRARYTABLE_GROUPING + ", " + + LIBRARYTABLE_YEAR + ", " + + LIBRARYTABLE_DURATION + ", " + + LIBRARYTABLE_BITRATE + ", " + + LIBRARYTABLE_SAMPLERATE + ", " + + LIBRARYTABLE_BPM + ", " + + LIBRARYTABLE_KEY + ", " + + LIBRARYTABLE_LOCATION + ", " + + LIBRARYTABLE_BPM_LOCK + ", " + + LIBRARYTABLE_DATETIMEADDED + + ", " "label, " "serato_db" ") VALUES (" @@ -629,9 +634,13 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat if (crateDir.cd(kCrateDirectory)) { QStringList filters; filters << kCrateFilter; - foreach(const QString& entry, crateDir.entryList(filters)) { + foreach (const QString& entry, crateDir.entryList(filters)) { QString crateFilePath = crateDir.filePath(entry); - QString crateName = parseCrate(database, databaseDir.path(), crateFilePath, trackIdMap); + QString crateName = parseCrate( + database, + databaseDir.path(), + crateFilePath, + trackIdMap); if (!crateName.isEmpty()) { QList data; data << QVariant(crateFilePath) @@ -765,52 +774,52 @@ void SeratoPlaylistModel::initSortColumnMapping() { m_columnIndexBySortColumnId[i] = -1; } - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] - = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] - = fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = + fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); m_sortColumnIdByColumnIndex.clear(); for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { @@ -890,10 +899,14 @@ SeratoFeature::SeratoFeature( clearTable(database, kSeratoLibraryTable); transaction.commit(); - connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, - this, &SeratoFeature::onSeratoDatabasesFound); - connect(&m_tracksFutureWatcher, &QFutureWatcher::finished, - this, &SeratoFeature::onTracksFound); + connect(&m_databasesFutureWatcher, + &QFutureWatcher>::finished, + this, + &SeratoFeature::onSeratoDatabasesFound); + connect(&m_tracksFutureWatcher, + &QFutureWatcher::finished, + this, + &SeratoFeature::onTracksFound); // initialize the model m_childModel.setRootItem(std::make_unique(this)); diff --git a/src/preferences/dialog/dlgpreflibrary.cpp b/src/preferences/dialog/dlgpreflibrary.cpp index 3baacf28dff..85ed2fb2a67 100644 --- a/src/preferences/dialog/dlgpreflibrary.cpp +++ b/src/preferences/dialog/dlgpreflibrary.cpp @@ -172,7 +172,7 @@ void DlgPrefLibrary::slotUpdate() { checkBox_show_rekordbox->setChecked(m_pConfig->getValue( ConfigKey("[Library]","ShowRekordboxLibrary"), true)); checkBox_show_serato->setChecked(m_pConfig->getValue( - ConfigKey("[Library]","ShowSeratoLibrary"), true)); + ConfigKey("[Library]", "ShowSeratoLibrary"), true)); switch (m_pConfig->getValue( ConfigKey("[Library]","TrackLoadAction"), LOAD_TO_DECK)) { @@ -313,8 +313,8 @@ void DlgPrefLibrary::slotApply() { ConfigValue((int)checkBox_show_traktor->isChecked())); m_pConfig->set(ConfigKey("[Library]","ShowRekordboxLibrary"), ConfigValue((int)checkBox_show_rekordbox->isChecked())); - m_pConfig->set(ConfigKey("[Library]","ShowSeratoLibrary"), - ConfigValue((int)checkBox_show_serato->isChecked())); + m_pConfig->set(ConfigKey("[Library]", "ShowSeratoLibrary"), + ConfigValue((int)checkBox_show_serato->isChecked())); int dbclick_status; if (radioButton_dbclick_bottom->isChecked()) { dbclick_status = ADD_TO_AUTODJ_BOTTOM; From 5fbc2fa0b6b2f105967555364c26cd3298afe395 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 22:16:11 +0100 Subject: [PATCH 20/31] library/serato/seratofeature: Fix Serato tables names --- src/library/serato/seratofeature.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 960ca3dd443..d759a4d9bb1 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -98,8 +98,8 @@ const QString kSmartCrateDirectory = QStringLiteral("Smart Crates"); const QString kSmartCrateFilter = QStringLiteral("*.scrate"); const QString kSeratoLibraryTable = QStringLiteral("serato_library"); -const QString kSeratoPlaylistsTable = QStringLiteral("serato_library"); -const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_library"); +const QString kSeratoPlaylistsTable = QStringLiteral("serato_playlists"); +const QString kSeratoPlaylistTracksTable = QStringLiteral("serato_playlist_tracks"); constexpr int kHeaderSize = 2*sizeof(quint32); From 7b96b03a6289e9f6af408b70d2a09d2c4d4d572b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 22:17:55 +0100 Subject: [PATCH 21/31] library/serato/seratofeature: Fix playlist names in TreeItems --- src/library/serato/seratofeature.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index d759a4d9bb1..1de09bb84bf 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -440,8 +440,8 @@ QString parseCrate(const QSqlDatabase& database, const QString& databasePath, QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* databaseItem) { QString databaseName = databaseItem->getLabel(); - QDir databaseDir = QDir(databaseItem->getData().toList()[0].toString()); - QString databaseFilePath = databaseDir.filePath(kDatabaseFilename); + QString databaseFilePath = databaseItem->getData().toList()[0].toString(); + QDir databaseDir = QFileInfo(databaseFilePath).dir(); QDir databaseRootDir = QDir(databaseDir); databaseRootDir.cdUp(); @@ -529,7 +529,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat return QString(); } - int playlistId = createPlaylist(database, databaseDir.path(), databaseDir.path()); + int playlistId = createPlaylist(database, databaseFilePath, databaseDir.path()); if (playlistId < 0) { qWarning() << "Failed to create library playlist for " << databaseFilePath; @@ -722,7 +722,7 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { foundDatabase->setLabel(displayPath); QList data; - data << QVariant(databaseDir.path()) + data << QVariant(databaseDir.filePath(kDatabaseFilename)) << QVariant(false); foundDatabase->setData(data); From 7eb72bef179b59982f669ec4d4ac022aa46223c3 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 22:19:10 +0100 Subject: [PATCH 22/31] library/serato/seratofeature: Switch from persistent to temporary tables --- res/schema.xml | 44 ---------- src/database/mixxxdb.cpp | 2 +- src/library/serato/seratofeature.cpp | 117 ++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 58 deletions(-) diff --git a/res/schema.xml b/res/schema.xml index a57b3b16f16..a82abfbf60c 100644 --- a/res/schema.xml +++ b/res/schema.xml @@ -480,49 +480,5 @@ METADATA position INTEGER ); - - - - Tables for Serato library feature - - - CREATE TABLE IF NOT EXISTS serato_library ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - comment TEXT, - grouping TEXT, - year INTEGER, - duration INTEGER, - bitrate TEXT, - samplerate TEXT, - bpm FLOAT, - key TEXT, - location TEXT, - bpm_lock INTEGER, - datetime_added DEFAULT CURRENT_TIMESTAMP, - label TEXT, - composer TEXT, - filename TEXT, - filetype TEXT, - remixer TEXT, - size INTEGER, - tracknumber TEXT, - serato_db TEXT - ); - CREATE TABLE IF NOT EXISTS serato_playlists ( - id INTEGER PRIMARY KEY, - name TEXT, - serato_db TEXT - ); - CREATE TABLE IF NOT EXISTS serato_playlist_tracks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - playlist_id INTEGER REFERENCES serato_playlists(id), - track_id INTEGER REFERENCES serato_library(id), - position INTEGER - ); - diff --git a/src/database/mixxxdb.cpp b/src/database/mixxxdb.cpp index 6dcd79a0a52..5a18ff73526 100644 --- a/src/database/mixxxdb.cpp +++ b/src/database/mixxxdb.cpp @@ -11,7 +11,7 @@ const QString MixxxDb::kDefaultSchemaFile(":/schema.xml"); //static -const int MixxxDb::kRequiredSchemaVersion = 31; +const int MixxxDb::kRequiredSchemaVersion = 30; namespace { diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 1de09bb84bf..93ad46186aa 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -732,23 +732,100 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { return foundDatabases; } -void clearTable(QSqlDatabase& database, QString tableName) { +bool createLibraryTable(QSqlDatabase& database, const QString& tableName) { + qDebug() << "Creating Serato library table: " << tableName; + QSqlQuery query(database); - query.prepare("DELETE FROM " + tableName); + query.prepare( + "CREATE TABLE IF NOT EXISTS " + tableName + + " (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " title TEXT," + " artist TEXT," + " album TEXT," + " genre TEXT," + " comment TEXT," + " grouping TEXT," + " year INTEGER," + " duration INTEGER," + " bitrate TEXT," + " samplerate TEXT," + " bpm FLOAT," + " key TEXT," + " location TEXT," + " bpm_lock INTEGER," + " datetime_added DEFAULT CURRENT_TIMESTAMP," + " label TEXT," + " composer TEXT," + " filename TEXT," + " filetype TEXT," + " remixer TEXT," + " size INTEGER," + " tracknumber TEXT," + " serato_db TEXT" + ");"); if (!query.exec()) { - LOG_FAILED_QUERY(query) << "tableName:" << tableName; - return; + LOG_FAILED_QUERY(query); + return false; } - query.prepare("DELETE FROM sqlite_sequence WHERE name=:name"); - query.bindValue(":name", tableName); + return true; +} + +bool createPlaylistsTable(QSqlDatabase& database, const QString& tableName) { + qDebug() << "Creating Serato playlists table: " << tableName; + + QSqlQuery query(database); + query.prepare( + "CREATE TABLE IF NOT EXISTS " + tableName + + " (" + " id INTEGER PRIMARY KEY," + " name TEXT," + " serato_db TEXT" + ");"); + if (!query.exec()) { - LOG_FAILED_QUERY(query) << "tableName:" << tableName; - return; + LOG_FAILED_QUERY(query); + return false; + } + + return true; +} + +bool createPlaylistTracksTable(QSqlDatabase& database, const QString& tableName) { + qDebug() << "Creating Serato playlist tracks table: " << tableName; + + QSqlQuery query(database); + query.prepare( + "CREATE TABLE IF NOT EXISTS " + tableName + + " (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " playlist_id INTEGER REFERENCES serato_playlists(id)," + " track_id INTEGER REFERENCES serato_library(id)," + " position INTEGER" + ");"); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + return true; +} + +bool dropTable(QSqlDatabase& database, QString tableName) { + qDebug() << "Dropping Serato table: " << tableName; + + QSqlQuery query(database); + query.prepare("DROP TABLE IF EXISTS " + tableName); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; } - qDebug() << "Serato table entries of '" << tableName << "' have been cleared."; + return true; } } // anonymous namespace @@ -882,12 +959,17 @@ SeratoFeature::SeratoFeature( m_title = tr("Serato"); - //Clear any previous Serato database entries if they exist QSqlDatabase database = m_pTrackCollection->database(); ScopedTransaction transaction(database); - clearTable(database, kSeratoPlaylistTracksTable); - clearTable(database, kSeratoPlaylistsTable); - clearTable(database, kSeratoLibraryTable); + // Drop any leftover temporary Serato database tables if they exist + dropTable(database, kSeratoPlaylistTracksTable); + dropTable(database, kSeratoPlaylistsTable); + dropTable(database, kSeratoLibraryTable); + + // Create new temporary Serato database tables + createLibraryTable(database, kSeratoLibraryTable); + createPlaylistsTable(database, kSeratoPlaylistsTable); + createPlaylistTracksTable(database, kSeratoPlaylistTracksTable); transaction.commit(); connect(&m_databasesFutureWatcher, &QFutureWatcher>::finished, @@ -902,6 +984,15 @@ SeratoFeature::SeratoFeature( SeratoFeature::~SeratoFeature() { m_databasesFuture.waitForFinished(); m_tracksFuture.waitForFinished(); + + // Drop temporary Serato database tables on shutdown + QSqlDatabase database = m_pTrackCollection->database(); + ScopedTransaction transaction(database); + dropTable(database, kSeratoPlaylistTracksTable); + dropTable(database, kSeratoPlaylistsTable); + dropTable(database, kSeratoLibraryTable); + transaction.commit(); + delete m_pSeratoPlaylistModel; } From c3c3e365fd8f2d22481c2597150562c0cba35541 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 5 Feb 2020 22:32:29 +0100 Subject: [PATCH 23/31] library/serato/seratofeature: Add icon to crate TreeItems --- src/library/serato/seratofeature.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 0719a213101..0cde4c3c1d2 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -645,7 +645,8 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat QList data; data << QVariant(crateFilePath) << QVariant(true); - databaseItem->appendChild(crateName, data); + TreeItem* crateItem = databaseItem->appendChild(crateName, data); + crateItem->setIcon(QIcon(":/images/library/ic_library_crates.svg")); } } } else { From fd4d1df2775007ae59b6c45daa43e45f86b7f5d5 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sun, 9 Feb 2020 23:40:17 +0100 Subject: [PATCH 24/31] library/serato/seratofeature: Fix Q_DISABLE_COPY(x) failures --- src/library/serato/seratofeature.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 0cde4c3c1d2..f84542168d9 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -360,7 +360,7 @@ QString parseCrate( return QString(); } - QFile crateFile = QFile(crateFilePath); + QFile crateFile(crateFilePath); if (!crateFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file " << crateFilePath @@ -404,7 +404,7 @@ QString parseCrate( break; } case FieldId::Track: { - QBuffer buffer = QBuffer(&data); + QBuffer buffer(&data); buffer.open(QIODevice::ReadOnly); QString location = parseCrateTrackPath(&buffer); if (!location.isEmpty()) { @@ -526,7 +526,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat ":serato_db" ")"); - QFile databaseFile = QFile(databaseFilePath); + QFile databaseFile(databaseFilePath); if (!databaseFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file " << databaseFilePath @@ -572,7 +572,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } case FieldId::Track: { serato_track_t track; - QBuffer buffer = QBuffer(&data); + QBuffer buffer(&data); buffer.open(QIODevice::ReadOnly); if (parseTrack(&track, &buffer)) { QString location = databaseRootDir.absoluteFilePath(track.location); From 63eed1637be7129c4d81f801f6b37233c233d5ac Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Tue, 11 Feb 2020 15:14:36 +0100 Subject: [PATCH 25/31] images/library/ic_library_serato.svg: Fix EOF in SVG image --- res/images/library/ic_library_serato.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/images/library/ic_library_serato.svg b/res/images/library/ic_library_serato.svg index b5a5c9380b7..bd99083931e 100644 --- a/res/images/library/ic_library_serato.svg +++ b/res/images/library/ic_library_serato.svg @@ -22,4 +22,4 @@ id="g6070"> \ No newline at end of file + id="path22" /> From 10b4eb7f4cc8fa833a5f53410a609f89564b5f92 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Wed, 12 Feb 2020 00:08:17 +0100 Subject: [PATCH 26/31] library/serato/seratofeature: Add size check for TreeItem data --- src/library/serato/seratofeature.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index f84542168d9..0e98420a83d 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -1111,6 +1111,9 @@ void SeratoFeature::activateChild(const QModelIndex& index) { // If the second element is false, then the database does still have to be // parsed. QList data = item->getData().toList(); + VERIFY_OR_DEBUG_ASSERT(data.size() == 2) { + return; + } QString playlist = data[0].toString(); bool isPlaylist = data[1].toBool(); From 84bce36d93d2864fded225836af1abd86398b71b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 15 Feb 2020 16:25:47 +0100 Subject: [PATCH 27/31] library/serato/seratofeature: Remove unnecessary transaction --- src/library/serato/seratofeature.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 0e98420a83d..1e71a44bd9b 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -1143,8 +1143,6 @@ void SeratoFeature::onSeratoDatabasesFound() { if (foundDatabases.size() == 0) { // No Serato databases found - ScopedTransaction transaction(database); - transaction.commit(); if (root->childRows() > 0) { // Devices have since been unmounted From b3de6215b62bbc4f8872f9af05bf476212f66f3b Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 15 Feb 2020 16:35:13 +0100 Subject: [PATCH 28/31] library/serato: Move SeratoPlaylistModel into separate file --- CMakeLists.txt | 1 + build/depends.py | 1 + src/library/serato/seratofeature.cpp | 84 --------------------- src/library/serato/seratofeature.h | 17 +---- src/library/serato/seratoplaylistmodel.cpp | 85 ++++++++++++++++++++++ src/library/serato/seratoplaylistmodel.h | 19 +++++ 6 files changed, 107 insertions(+), 100 deletions(-) create mode 100644 src/library/serato/seratoplaylistmodel.cpp create mode 100644 src/library/serato/seratoplaylistmodel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 10e694ae4b6..afce21f6e4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,6 +399,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/library/searchquery.cpp src/library/searchqueryparser.cpp src/library/serato/seratofeature.cpp + src/library/serato/seratoplaylistmodel.cpp src/library/setlogfeature.cpp src/library/sidebarmodel.cpp src/library/songdownloader.cpp diff --git a/build/depends.py b/build/depends.py index 25ce01fcef4..c4eb5bdb5c6 100644 --- a/build/depends.py +++ b/build/depends.py @@ -1082,6 +1082,7 @@ def sources(self, build): "src/library/itunes/itunesfeature.cpp", "src/library/traktor/traktorfeature.cpp", "src/library/serato/seratofeature.cpp", + "src/library/serato/seratoplaylistmodel.cpp", "src/library/rekordbox/rekordboxfeature.cpp", "src/library/rekordbox/rekordbox_pdb.cpp", diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 1e71a44bd9b..7d7c2d84e66 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -840,90 +840,6 @@ bool dropTable(QSqlDatabase& database, QString tableName) { } // anonymous namespace -SeratoPlaylistModel::SeratoPlaylistModel(QObject* parent, - TrackCollectionManager* trackCollectionManager, - QSharedPointer trackSource) - : BaseExternalPlaylistModel(parent, trackCollectionManager, "mixxx.db.model.serato.playlistmodel", "serato_playlists", "serato_playlist_tracks", trackSource) { -} - -void SeratoPlaylistModel::initSortColumnMapping() { - // Add a bijective mapping between the SortColumnIds and column indices - for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { - m_columnIndexBySortColumnId[i] = -1; - } - - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = - fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); - m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = - fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); - - m_sortColumnIdByColumnIndex.clear(); - for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { - TrackModel::SortColumnId sortColumn = static_cast(i); - m_sortColumnIdByColumnIndex.insert(m_columnIndexBySortColumnId[sortColumn], sortColumn); - } -} - -TrackPointer SeratoPlaylistModel::getTrack(const QModelIndex& index) const { - qDebug() << "SeratoTrackModel::getTrack"; - - TrackPointer track = BaseExternalPlaylistModel::getTrack(index); - - return track; -} - -bool SeratoPlaylistModel::isColumnHiddenByDefault(int column) { - if ( - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) || - column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID)) { - return true; - } - return BaseSqlTableModel::isColumnHiddenByDefault(column); -} - SeratoFeature::SeratoFeature( Library* pLibrary, UserSettingsPointer pConfig) diff --git a/src/library/serato/seratofeature.h b/src/library/serato/seratofeature.h index fa34459a1eb..c917b07a65d 100644 --- a/src/library/serato/seratofeature.h +++ b/src/library/serato/seratofeature.h @@ -18,25 +18,10 @@ #include #include "library/baseexternallibraryfeature.h" -#include "library/baseexternalplaylistmodel.h" #include "library/baseexternaltrackmodel.h" +#include "library/serato/seratoplaylistmodel.h" #include "library/treeitemmodel.h" -class TrackCollectionManager; -class BaseExternalPlaylistModel; - -class SeratoPlaylistModel : public BaseExternalPlaylistModel { - public: - SeratoPlaylistModel(QObject* parent, - TrackCollectionManager* pTrackCollectionManager, - QSharedPointer trackSource); - TrackPointer getTrack(const QModelIndex& index) const override; - bool isColumnHiddenByDefault(int column) override; - - protected: - void initSortColumnMapping() override; -}; - class SeratoFeature : public BaseExternalLibraryFeature { Q_OBJECT public: diff --git a/src/library/serato/seratoplaylistmodel.cpp b/src/library/serato/seratoplaylistmodel.cpp new file mode 100644 index 00000000000..8c038b480e1 --- /dev/null +++ b/src/library/serato/seratoplaylistmodel.cpp @@ -0,0 +1,85 @@ +#include "library/serato/seratoplaylistmodel.h" + +SeratoPlaylistModel::SeratoPlaylistModel(QObject* parent, + TrackCollectionManager* trackCollectionManager, + QSharedPointer trackSource) + : BaseExternalPlaylistModel(parent, trackCollectionManager, "mixxx.db.model.serato.playlistmodel", "serato_playlists", "serato_playlist_tracks", trackSource) { +} + +void SeratoPlaylistModel::initSortColumnMapping() { + // Add a bijective mapping between the SortColumnIds and column indices + for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { + m_columnIndexBySortColumnId[i] = -1; + } + + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TITLE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TITLE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_ALBUMARTIST] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ALBUMARTIST); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_YEAR] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GENRE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMPOSER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_GROUPING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GROUPING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TRACKNUMBER] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_FILETYPE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_NATIVELOCATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COMMENT] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMMENT); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DURATION] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BITRATE] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_PREVIEW] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_PREVIEW); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_COVERART] = + fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART); + m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_POSITION] = + fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_POSITION); + + m_sortColumnIdByColumnIndex.clear(); + for (int i = 0; i < TrackModel::SortColumnId::NUM_SORTCOLUMNIDS; ++i) { + TrackModel::SortColumnId sortColumn = static_cast(i); + m_sortColumnIdByColumnIndex.insert(m_columnIndexBySortColumnId[sortColumn], sortColumn); + } +} + +TrackPointer SeratoPlaylistModel::getTrack(const QModelIndex& index) const { + qDebug() << "SeratoTrackModel::getTrack"; + + TrackPointer track = BaseExternalPlaylistModel::getTrack(index); + + return track; +} + +bool SeratoPlaylistModel::isColumnHiddenByDefault(int column) { + if ( + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BITRATE) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK) || + column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_ID)) { + return true; + } + return BaseSqlTableModel::isColumnHiddenByDefault(column); +} diff --git a/src/library/serato/seratoplaylistmodel.h b/src/library/serato/seratoplaylistmodel.h new file mode 100644 index 00000000000..18a73b311f0 --- /dev/null +++ b/src/library/serato/seratoplaylistmodel.h @@ -0,0 +1,19 @@ +#pragma once +// seratoplaylistmodel.h +// Created 2020-02-15 by Jan Holthuis +#include "library/baseexternalplaylistmodel.h" + +class TrackCollectionManager; +class BaseExternalPlaylistModel; + +class SeratoPlaylistModel : public BaseExternalPlaylistModel { + public: + SeratoPlaylistModel(QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + QSharedPointer trackSource); + TrackPointer getTrack(const QModelIndex& index) const override; + bool isColumnHiddenByDefault(int column) override; + + protected: + void initSortColumnMapping() override; +}; From 9e510b067cdca8191f356f900189be39fd945cda Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 15 Feb 2020 16:37:34 +0100 Subject: [PATCH 29/31] library/serato/seratofeature: Rename bytesToQString to utf16beToQString --- src/library/serato/seratofeature.cpp | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 7d7c2d84e66..0c8c82be3c3 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -136,7 +136,7 @@ int insertTrackIntoPlaylist(const QSqlDatabase& database, int playlistId, int tr return query.lastInsertId().toInt(); } -inline QString bytesToQString(const QByteArray& data, const quint32 size) { +inline QString utf16beToQString(const QByteArray& data, const quint32 size) { return QTextCodec::codecForName("UTF-16BE")->toUnicode(data, size); } @@ -180,65 +180,65 @@ inline bool parseTrack(serato_track_t* track, QIODevice* buffer) { // Parse field data switch (static_cast(fieldId)) { case FieldId::FileType: - track->filetype = bytesToQString(data, fieldSize); + track->filetype = utf16beToQString(data, fieldSize); break; case FieldId::FilePath: - track->location = bytesToQString(data, fieldSize); + track->location = utf16beToQString(data, fieldSize); break; case FieldId::SongTitle: - track->title = bytesToQString(data, fieldSize); + track->title = utf16beToQString(data, fieldSize); break; case FieldId::Artist: - track->artist = bytesToQString(data, fieldSize); + track->artist = utf16beToQString(data, fieldSize); break; case FieldId::Album: - track->album = bytesToQString(data, fieldSize); + track->album = utf16beToQString(data, fieldSize); break; case FieldId::Genre: - track->genre = bytesToQString(data, fieldSize); + track->genre = utf16beToQString(data, fieldSize); break; case FieldId::Length: { bool ok; - int duration = bytesToQString(data, fieldSize).toInt(&ok); + int duration = utf16beToQString(data, fieldSize).toInt(&ok); if (ok) { track->duration = duration; } break; } case FieldId::Bitrate: - track->bitrate = bytesToQString(data, fieldSize); + track->bitrate = utf16beToQString(data, fieldSize); break; case FieldId::SampleRate: - track->samplerate = bytesToQString(data, fieldSize); + track->samplerate = utf16beToQString(data, fieldSize); break; case FieldId::Bpm: { bool ok; - double bpm = bytesToQString(data, fieldSize).toDouble(&ok); + double bpm = utf16beToQString(data, fieldSize).toDouble(&ok); if (ok) { track->bpm = bpm; } break; } case FieldId::Comment: - track->comment = bytesToQString(data, fieldSize); + track->comment = utf16beToQString(data, fieldSize); break; case FieldId::Grouping: - track->grouping = bytesToQString(data, fieldSize); + track->grouping = utf16beToQString(data, fieldSize); break; case FieldId::Label: - track->label = bytesToQString(data, fieldSize); + track->label = utf16beToQString(data, fieldSize); break; case FieldId::Year: { // 4-digit year as string (YYYY) bool ok; - int year = bytesToQString(data, fieldSize).toInt(&ok); + int year = utf16beToQString(data, fieldSize).toInt(&ok); if (ok) { track->year = year; } break; } case FieldId::Key: - track->key = bytesToQString(data, fieldSize); + track->key = utf16beToQString(data, fieldSize); break; case FieldId::BeatgridLocked: track->beatgridlocked = bytesToBoolean(data); @@ -318,7 +318,7 @@ inline QString parseCrateTrackPath(QIODevice* buffer) { // Parse field data switch (static_cast(fieldId)) { case FieldId::TrackPath: - location = bytesToQString(data, fieldSize); + location = utf16beToQString(data, fieldSize); break; default: { QString fieldName = QString(headerData.mid(0, sizeof(quint32))); @@ -398,7 +398,7 @@ QString parseCrate( // Parse field data switch (static_cast(fieldId)) { case FieldId::Version: { - QString version = bytesToQString(data, fieldSize); + QString version = utf16beToQString(data, fieldSize); qDebug() << "Serato Database Version: " << version; break; @@ -565,7 +565,7 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat // Parse field data switch (static_cast(fieldId)) { case FieldId::Version: { - QString version = bytesToQString(data, fieldSize); + QString version = utf16beToQString(data, fieldSize); qDebug() << "Serato Database Version: " << version; break; From 13914af17942b081ab915a39c4b014e66a62db37 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 15 Feb 2020 16:39:39 +0100 Subject: [PATCH 30/31] library/serato/seratoplaylistmodel: Wrap long line manually --- src/library/serato/seratoplaylistmodel.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/library/serato/seratoplaylistmodel.cpp b/src/library/serato/seratoplaylistmodel.cpp index 8c038b480e1..cb8be77a10f 100644 --- a/src/library/serato/seratoplaylistmodel.cpp +++ b/src/library/serato/seratoplaylistmodel.cpp @@ -3,7 +3,13 @@ SeratoPlaylistModel::SeratoPlaylistModel(QObject* parent, TrackCollectionManager* trackCollectionManager, QSharedPointer trackSource) - : BaseExternalPlaylistModel(parent, trackCollectionManager, "mixxx.db.model.serato.playlistmodel", "serato_playlists", "serato_playlist_tracks", trackSource) { + : BaseExternalPlaylistModel( + parent, + trackCollectionManager, + "mixxx.db.model.serato.playlistmodel", + "serato_playlists", + "serato_playlist_tracks", + trackSource) { } void SeratoPlaylistModel::initSortColumnMapping() { From 627d112bf84194148dcf750acd2c6668fb607d87 Mon Sep 17 00:00:00 2001 From: Jan Holthuis Date: Sat, 15 Feb 2020 21:28:07 +0100 Subject: [PATCH 31/31] library/serato/seratofeature: Switch to new TreeItem initialization --- src/library/serato/seratofeature.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/library/serato/seratofeature.cpp b/src/library/serato/seratofeature.cpp index 0c8c82be3c3..212c1da774d 100644 --- a/src/library/serato/seratofeature.cpp +++ b/src/library/serato/seratofeature.cpp @@ -662,12 +662,10 @@ QString parseDatabase(mixxx::DbConnectionPoolPtr dbConnectionPool, TreeItem* dat } // This function is executed in a separate thread other than the main thread -QList findSeratoDatabases(SeratoFeature* seratoFeature) { +QList findSeratoDatabases() { QThread* thisThread = QThread::currentThread(); thisThread->setPriority(QThread::LowPriority); - QList foundDatabases; - // Build a list of directories that could contain the _Serato_ directory QFileInfoList databaseLocations; foreach (const QString& musicDir, QStandardPaths::standardLocations(QStandardPaths::MusicLocation)) { @@ -712,6 +710,7 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { volumesDir.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot)); #endif + QList foundDatabases; foreach (QFileInfo databaseLocation, databaseLocations) { QDir databaseDir = QDir(databaseLocation.filePath()); if (!databaseDir.cd(kDatabaseDirectory)) { @@ -722,19 +721,19 @@ QList findSeratoDatabases(SeratoFeature* seratoFeature) { continue; } - TreeItem* foundDatabase = new TreeItem(seratoFeature); - QString displayPath = databaseLocation.filePath(); if (displayPath.endsWith("/")) { displayPath.chop(1); } - foundDatabase->setLabel(displayPath); QList data; data << QVariant(databaseDir.filePath(kDatabaseFilename)) << QVariant(false); - foundDatabase->setData(data); + + TreeItem* foundDatabase = new TreeItem( + std::move(displayPath), + QVariant(data)); foundDatabases << foundDatabase; } @@ -908,7 +907,7 @@ SeratoFeature::SeratoFeature( &SeratoFeature::onTracksFound); // initialize the model - m_childModel.setRootItem(std::make_unique(this)); + m_childModel.setRootItem(TreeItem::newRoot(this)); } SeratoFeature::~SeratoFeature() { @@ -999,7 +998,7 @@ void SeratoFeature::activate() { qDebug() << "SeratoFeature::activate()"; // Let a worker thread do the parsing - m_databasesFuture = QtConcurrent::run(findSeratoDatabases, this); + m_databasesFuture = QtConcurrent::run(findSeratoDatabases); m_databasesFutureWatcher.setFuture(m_databasesFuture); m_title = tr("(loading) Serato"); //calls a slot in the sidebar model such that 'Serato (isLoading)' is displayed.