diff options
-rw-r--r-- | CMakeLists.txt | 16 | ||||
-rw-r--r-- | src/library/export/dlglibraryexport.cpp | 209 | ||||
-rw-r--r-- | src/library/export/dlglibraryexport.h | 64 | ||||
-rw-r--r-- | src/library/export/engineprimeexportjob.cpp | 539 | ||||
-rw-r--r-- | src/library/export/engineprimeexportjob.h | 68 | ||||
-rw-r--r-- | src/library/export/engineprimeexportrequest.h | 18 | ||||
-rw-r--r-- | src/library/export/exportrequest.h | 22 | ||||
-rw-r--r-- | src/library/export/libraryexporter.cpp | 60 | ||||
-rw-r--r-- | src/library/export/libraryexporter.h | 49 | ||||
-rw-r--r-- | src/library/library.cpp | 24 | ||||
-rw-r--r-- | src/library/library.h | 17 | ||||
-rw-r--r-- | src/library/mixxxlibraryfeature.cpp | 33 | ||||
-rw-r--r-- | src/library/mixxxlibraryfeature.h | 39 | ||||
-rw-r--r-- | src/library/trackset/crate/cratefeature.cpp | 38 | ||||
-rw-r--r-- | src/library/trackset/crate/cratefeature.h | 14 | ||||
-rw-r--r-- | src/mixxx.cpp | 33 | ||||
-rw-r--r-- | src/mixxx.h | 11 | ||||
-rw-r--r-- | src/widget/wmainmenubar.cpp | 11 | ||||
-rw-r--r-- | src/widget/wmainmenubar.h | 3 |
19 files changed, 1255 insertions, 13 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index ccfbb72072..f0fb245ed8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1481,14 +1481,12 @@ endif() # Denon Engine Prime library export support (using libdjinterop) option(ENGINEPRIME "Support for library export to Denon Engine Prime" OFF) if(ENGINEPRIME) - target_compile_definitions(mixxx-lib PUBLIC __ENGINEPRIME__) - # Look for an existing installation of libdjinterop and use that if available. # Otherwise, download and build from GitHub. find_package(DjInterop) if(DjInterop_FOUND) # An existing installation of djinterop is available. - message(STATUS "Linking dynamically to existing installation of libdjinterop") + message(STATUS "Using existing system installation of libdjinterop") target_include_directories(mixxx-lib PUBLIC ${DjInterop_INCLUDE_DIRS}) target_link_libraries(mixxx-lib PUBLIC DjInterop::DjInterop) else() @@ -1533,11 +1531,19 @@ if(ENGINEPRIME) # Since we have built libdjinterop from sources as a static library, its # transitive dependencies are not automatically recognised. libdjinterop - # depends on zlib and sqlite3. Mixxx already has a dependency on sqlite3, - # but not zlib, so we explicitly add that here. + # depends on zlib and optionally sqlite3. If libdjinterop was configured + # to depend on system SQLite, Mixxx will already have the dependency. + # But it does not have zlib, so we explicitly add that here. find_package(ZLIB 1.2.8 REQUIRED) target_link_libraries(mixxx-lib PUBLIC ${ZLIB_LIBRARIES}) endif() + + # Include conditional sources only required with Engine Prime export support. + target_sources(mixxx-lib PRIVATE + src/library/export/dlglibraryexport.cpp + src/library/export/engineprimeexportjob.cpp + src/library/export/libraryexporter.cpp) + target_compile_definitions(mixxx-lib PUBLIC __ENGINEPRIME__) endif() # Ebur128 diff --git a/src/library/export/dlglibraryexport.cpp b/src/library/export/dlglibraryexport.cpp new file mode 100644 index 0000000000..54992b3817 --- /dev/null +++ b/src/library/export/dlglibraryexport.cpp @@ -0,0 +1,209 @@ +#include "library/export/dlglibraryexport.h" + +#include <QFileDialog> +#include <QFormLayout> +#include <QGridLayout> +#include <QHBoxLayout> +#include <QLabel> +#include <QPushButton> +#include <QStandardPaths> +#include <djinterop/enginelibrary.hpp> + +#include "library/export/engineprimeexportrequest.h" +#include "library/trackcollection.h" +#include "library/trackcollectionmanager.h" +#include "library/trackset/crate/crateid.h" +#include "library/trackset/crate/cratestorage.h" + +namespace el = djinterop::enginelibrary; + +namespace mixxx { + +namespace { +const QString kDefaultMixxxExportDirName = QStringLiteral("mixxx-export"); + +void populateCrates( + QListWidget& listWidget, + const TrackCollection& trackCollection) { + // Populate list of crates. + CrateSelectResult crates = trackCollection.crates().selectCrates(); + Crate crate; + while (crates.populateNext(&crate)) { + auto pItem = std::make_unique<QListWidgetItem>(crate.getName()); + pItem->setData(Qt::UserRole, crate.getId().value()); + listWidget.addItem(pItem.release()); + } +} +} // namespace + +DlgLibraryExport::DlgLibraryExport( + QWidget* parent, + UserSettingsPointer pConfig, + TrackCollectionManager* pTrackCollectionManager) + : QDialog(parent), + m_pConfig{pConfig}, + m_pTrackCollectionManager{pTrackCollectionManager} { + // Selectable list of crates from the Mixxx library. + m_pCratesList = make_parented<QListWidget>(); + m_pCratesList->setSelectionMode(QListWidget::ExtendedSelection); + populateCrates(*m_pCratesList, *m_pTrackCollectionManager->internalCollection()); + + // Read-only text fields showing key directories for export. + m_pBaseDirectoryTextField = make_parented<QLineEdit>(); + m_pBaseDirectoryTextField->setReadOnly(true); + m_pDatabaseDirectoryTextField = make_parented<QLineEdit>(); + m_pDatabaseDirectoryTextField->setReadOnly(true); + m_pMusicDirectoryTextField = make_parented<QLineEdit>(); + m_pMusicDirectoryTextField->setReadOnly(true); + + // Radio buttons to allow choice between exporting the whole music library + // or just tracks in a selection of crates. + m_pWholeLibraryRadio = make_parented<QRadioButton>(tr("Entire music library")); + m_pWholeLibraryRadio->setChecked(true); + m_pCratesList->setEnabled(false); + connect(m_pWholeLibraryRadio, + &QRadioButton::clicked, + this, + [this]() { m_pCratesList->setEnabled(false); }); + m_pCratesRadio = make_parented<QRadioButton>(tr("Selected crates")); + connect(m_pCratesRadio, + &QRadioButton::clicked, + this, + [this]() { m_pCratesList->setEnabled(true); }); + + // Button to allow ability to browse for the export directory. + auto pExportDirBrowseButton = make_parented<QPushButton>(tr("Browse")); + connect(pExportDirBrowseButton, + &QPushButton::clicked, + this, + &DlgLibraryExport::browseExportDirectory); + auto pExportDirLayout = make_parented<QHBoxLayout>(); + pExportDirLayout->addWidget(m_pBaseDirectoryTextField); + pExportDirLayout->addWidget(pExportDirBrowseButton); + + auto pFormLayout = make_parented<QFormLayout>(); + pFormLayout->addRow(tr("Base export directory"), pExportDirLayout); + pFormLayout->addRow(tr("Engine Prime database export directory"), + m_pDatabaseDirectoryTextField); + pFormLayout->addRow(tr("Copy music files to"), m_pMusicDirectoryTextField); + + // Buttons to begin the export or cancel. + auto pExportButton = make_parented<QPushButton>(tr("Export")); + pExportButton->setDefault(true); + connect(pExportButton, &QPushButton::clicked, this, &DlgLibraryExport::exportRequested); + auto pCancelButton = make_parented<QPushButton>(tr("Cancel")); + connect(pCancelButton, &QPushButton::clicked, this, &QDialog::reject); + + // Arrange action buttons at bottom of dialog. + auto pButtonBarLayout = make_parented<QHBoxLayout>(); + pButtonBarLayout->addStretch(1); + pButtonBarLayout->addWidget(pExportButton); + pButtonBarLayout->addWidget(pCancelButton); + + auto pLayout = make_parented<QGridLayout>(); + pLayout->setColumnStretch(0, 1); + pLayout->setColumnStretch(1, 2); + pLayout->addWidget(m_pWholeLibraryRadio, 0, 0); + pLayout->addWidget(m_pCratesRadio, 1, 0); + pLayout->addWidget(m_pCratesList, 2, 0); + pLayout->addLayout(pFormLayout, 0, 1, 3, 1); + pLayout->addLayout(pButtonBarLayout, 3, 0, 1, 2); + + setLayout(pLayout); + setWindowTitle(tr("Export Library")); + + show(); + raise(); + activateWindow(); +} + +void DlgLibraryExport::setSelectedCrate(std::optional<CrateId> crateId) { + if (!crateId) { + m_pWholeLibraryRadio->setChecked(true); + m_pCratesList->setEnabled(false); + return; + } + + m_pCratesRadio->setChecked(true); + m_pCratesList->setEnabled(true); + for (auto i = 0; i < m_pCratesList->count(); ++i) { + auto* pItem = m_pCratesList->item(i); + auto currCrateId = pItem->data(Qt::UserRole).toInt(); + if (currCrateId == crateId.value().value()) { + m_pCratesList->setCurrentItem(pItem); + return; + } + } +} + +void DlgLibraryExport::browseExportDirectory() { + QString lastExportDirectory = + m_pConfig->getValue(ConfigKey("[Library]", "LastLibraryExportDirectory"), + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + auto baseDirectory = + QFileDialog::getExistingDirectory(NULL, tr("Export Library To"), lastExportDirectory); + if (baseDirectory.isEmpty()) { + return; + } + m_pConfig->set( + ConfigKey("[Library]", "LastLibraryExportDirectory"), ConfigValue(baseDirectory)); + + QDir baseExportDirectory{baseDirectory}; + auto databaseDirectory = baseExportDirectory.filePath( + el::default_database_dir_name); + auto musicDirectory = baseExportDirectory.filePath(kDefaultMixxxExportDirName); + + m_pBaseDirectoryTextField->setText(baseDirectory); + m_pDatabaseDirectoryTextField->setText(databaseDirectory); + m_pMusicDirectoryTextField->setText(musicDirectory); +} + +void DlgLibraryExport::exportRequested() { + // Check a base export directory has been chosen + if (m_pBaseDirectoryTextField->text() == "") { + QMessageBox::information(this, + tr("No Export Directory Chosen"), + tr("No export directory was chosen. Please choose a directory " + "in order to export the music library."), + QMessageBox::Ok, + QMessageBox::Ok); + return; + } + + // See if an EL DB exists in the chosen dir already, and ask the user for + // confirmation before proceeding if so. + if (el::database_exists(m_pDatabaseDirectoryTextField->text().toStdString())) { + int ret = QMessageBox::question( + this, + tr("Merge Into Existing Library?"), + tr("There is already an existing library in directory ") + + m_pDatabaseDirectoryTextField->text() + + tr("\nIf you proceed, the Mixxx library will be merged into " + "this existing library. Do you want to merge into the " + "the existing library?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (ret != QMessageBox::Yes) { + return; + } + } + + // Construct a request to export the library/crates. + // Assumed to always be an Engine Prime export in this iteration of the + // dialog. + EnginePrimeExportRequest request; + request.engineLibraryDbDir = QDir{m_pDatabaseDirectoryTextField->text()}; + request.musicFilesDir = QDir{m_pMusicDirectoryTextField->text()}; + request.exportSelectedCrates = m_pCratesList->isEnabled(); + if (request.exportSelectedCrates) { + for (auto* pItem : m_pCratesList->selectedItems()) { + CrateId id{pItem->data(Qt::UserRole).value<int>()}; + request.crateIdsToExport.insert(id); + } + } + + emit startEnginePrimeExport(std::move(request)); + accept(); +} + +} // namespace mixxx diff --git a/src/library/export/dlglibraryexport.h b/src/library/export/dlglibraryexport.h new file mode 100644 index 0000000000..364acb2b94 --- /dev/null +++ b/src/library/export/dlglibraryexport.h @@ -0,0 +1,64 @@ +#pragma once + +#include <QDialog> +#include <QLineEdit> +#include <QListWidget> +#include <QRadioButton> +#include <QTreeWidget> +#include <QWidget> +#include <memory> + +#include "library/export/engineprimeexportrequest.h" +#include "library/trackset/crate/crateid.h" +#include "preferences/usersettings.h" +#include "util/optional.h" +#include "util/parented_ptr.h" + +class TrackCollectionManager; + +namespace mixxx { + +/// The DlgLibraryExport class is a UI window that gathers information from +/// the user about how they would like to export the Mixxx library. +/// +/// Currently, the dialog only supports exporting to the Engine Library format, +/// but in future it is expected that this dialog could be expanded to include +/// other formats, and generate different export signals accordingly. +class DlgLibraryExport : public QDialog { + Q_OBJECT + + public: + DlgLibraryExport( + QWidget* parent, + UserSettingsPointer pConfig, + TrackCollectionManager* pTrackCollectionManager); + + /// Set the specified crate to be selected for export on the dialog. If no + /// crate is provided (i.e. `std::nullopt`), then the dialog will be ready + // to export the whole library. If an unknown crate is provided, then no + /// action is taken. + void setSelectedCrate(std::optional<CrateId> crateId); + + signals: + /// The startEnginePrimeExport signal is emitted when sufficient information + /// has been gathered from the user to kick off an Engine Prime export, and + /// details of the request are provided as part of the signal. + void startEnginePrimeExport(EnginePrimeExportRequest) const; + + private slots: + void browseExportDirectory(); + void exportRequested(); + + private: + UserSettingsPointer m_pConfig; + TrackCollectionManager* m_pTrackCollectionManager; + + parented_ptr<QRadioButton> m_pWholeLibraryRadio; + parented_ptr<QRadioButton> m_pCratesRadio; + parented_ptr<QListWidget> m_pCratesList; + parented_ptr<QLineEdit> m_pBaseDirectoryTextField; + parented_ptr<QLineEdit> m_pDatabaseDirectoryTextField; + parented_ptr<QLineEdit> m_pMusicDirectoryTextField; +}; + +} // namespace mixxx diff --git a/src/library/export/engineprimeexportjob.cpp b/src/library/export/engineprimeexportjob.cpp new file mode 100644 index 0000000000..b6d4e8dad9 --- /dev/null +++ b/src/library/export/engineprimeexportjob.cpp @@ -0,0 +1,539 @@ +#include "library/export/engineprimeexportjob.h" + +#include <QHash> +#include <QMetaMethod> +#include <QStringList> +#include <array> +#include <cstdint> +#include <djinterop/djinterop.hpp> +#include <memory> +#include <optional> +#include <stdexcept> + +#include "library/trackcollection.h" +#include "library/trackset/crate/crate.h" +#include "track/track.h" +#include "waveform/waveformfactory.h" + +namespace el = djinterop::enginelibrary; + +namespace mixxx { + +namespace { + +const std::string kMixxxRootCrateName = "Mixxx"; + +const QStringList kSupportedFileTypes = {"mp3", "flac", "ogg"}; + +std::optional<djinterop::musical_key> toDjinteropKey( + track::io::key::ChromaticKey key) { + static const std::array<std::optional<djinterop::musical_key>, 25> keyMap{{ + std::nullopt, // INVALID = 0, + djinterop::musical_key::c_major, // C_MAJOR = 1, + djinterop::musical_key::d_flat_major, // D_FLAT_MAJOR = 2, + djinterop::musical_key::d_major, // D_MAJOR = 3, + djinterop::musical_key::e_flat_major, // E_FLAT_MAJOR = 4, + djinterop::musical_key::e_major, // E_MAJOR = 5, + djinterop::musical_key::f_major, // F_MAJOR = 6, + djinterop::musical_key::f_sharp_major, // F_SHARP_MAJOR = 7, + djinterop::musical_key::g_major, // G_MAJOR = 8, + djinterop::musical_key::a_flat_major, // A_FLAT_MAJOR = 9, + djinterop::musical_key::a_major, // A_MAJOR = 10, + djinterop::musical_key::b_flat_major, // B_FLAT_MAJOR = 11, + djinterop::musical_key::b_major, // B_MAJOR = 12, + djinterop::musical_key::c_minor, // C_MINOR = 13, + djinterop::musical_key::d_flat_minor, // C_SHARP_MINOR = 14, + djinterop::musical_key::d_minor, // D_MINOR = 15, + djinterop::musical_key::e_flat_minor, // E_FLAT_MINOR = 16, + djinterop::musical_key::e_minor, // E_MINOR = 17, + djinterop::musical_key::f_minor, // F_MINOR = 18, + djinterop::musical_key::f_sharp_minor, // F_SHARP_MINOR = 19, + djinterop::musical_key::g_minor, // G_MINOR = 20, + djinterop::musical_key::a_flat_minor, // G_SHARP_MINOR = 21, + djinterop::musical_key::a_minor, // A_MINOR = 22, + djinterop::musical_key::b_flat_minor, // B_FLAT_MINOR = 23, + djinterop::musical_key::b_minor, // B_MINOR = 24 + }}; + + return keyMap[key]; +} + +std::string exportFile(const EnginePrimeExportRequest& request, + TrackPointer pTrack) { + if (!request.engineLibraryDbDir.exists()) { + auto msg = QString( + "Engine Library DB directory %1 has been removed from disk!") + .arg(request.engineLibraryDbDir.absolutePath()); + throw std::runtime_error{msg.toStdString()}; + } else if (!request.musicFilesDir.exists()) { + auto msg = QString( + "Music file export directory %1 has been removed from disk!") + .arg(request.musicFilesDir.absolutePath()); + throw std::runtime_error{msg.toStdString()}; + } + + // Copy music files into the Mixxx export dir, if the source file has + // been modified (or the destination doesn't exist). To ensure no + // chance of filename clashes, and to keep things simple, we will prefix + // the destination files with the DB track identifier. + TrackFile srcFileInfo = pTrack->getFileInfo(); + auto trackId = pTrack->getId().value(); + QString dstFilename = QString::number(trackId) + " - " + srcFileInfo.fileName(); + QString dstPath = request.musicFilesDir.filePath(dstFilename); + if (!QFile::exists(dstPath) || + srcFileInfo.fileLastModified() > QFileInfo{dstPath}.lastModified()) { + auto srcPath = srcFileInfo.location(); + QFile::copy(srcPath, dstPath); + } + + return request.engineLibraryDbDir.relativeFilePath(dstPath).toStdString(); +} + +djinterop::track getTrackByRelativePath( + djinterop::database database, const std::string& relativePath) { + auto trackCandidates = database.tracks_by_relative_path(relativePath); + switch (trackCandidates.size()) { + case 0: + return database.create_track(relativePath); + case 1: + return trackCandidates.front(); + default: + qInfo() << "Warning: More than one external track with the same relative path"; + return trackCandidates.front(); + } +} + +void exportMetadata(djinterop::database& db, + QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap, + TrackPointer pTrack, + std::unique_ptr<Waveform> pWaveform, + const std::string& relativePath) { + // Create or load the track in the database, using the relative path to + // the music file. We will record the mapping from Mixxx track id to + // exported track id as well. + auto externalTrack = getTrackByRelativePath(db, relativePath); + mixxxToEnginePrimeTrackIdMap.insert(pTrack->getId(), externalTrack.id()); + + // Note that the Engine Prime format has the scope for recording meta-data + // about whether track was imported from an external database. However, + // that meta-data only extends as far as other Engine Prime databases, + // which Mixxx is not. So we do not set any import information on the + // exported track. + externalTrack.set_track_number(pTrack->getTrackNumber().toInt()); + externalTrack.set_bpm(pTrack->getBpm()); + externalTrack.set_year(pTrack->getYear().toInt()); + externalTrack.set_title(pTrack->getTitle().toStdString()); + externalTrack.set_artist(pTrack->getArtist().toStdString()); + externalTrack.set_album(pTrack->getAlbum().toStdString()); + externalTrack.set_genre(pTrack->getGenre().toStdString()); + externalTrack.set_comment(pTrack->getComment().toStdString()); + externalTrack.set_composer(pTrack->getComposer().toStdString()); + externalTrack.set_key(toDjinteropKey(pTrack->getKey())); + int64_t lastModifiedMillisSinceEpoch = + pTrack->getFileInfo().fileLastModified().toMSecsSinceEpoch(); + std::chrono::system_clock::time_point lastModifiedAt{ + std::chrono::milliseconds{lastModifiedMillisSinceEpoch}}; + externalTrack.set_last_modified_at(lastModifiedAt); + externalTrack.set_bitrate(pTrack->getBitrate()); + + // Frames used interchangeably with "samples" here. + auto sampleCount = static_cast<int64_t>(pTrack->getDuration() * pTrack->getSampleRate()); + externalTrack.set_sampling({static_cast<double>(pTrack->getSampleRate()), sampleCount}); + + // Set track loudness. + // Note that the djinterop API method for setting loudness may be revised + // in future, as more is discovered about the exact meaning of the loudness + // field in the Engine Library format. Make the assumption for now that + // ReplayGain ratio is an appropriate value to set, which has been validated + // by basic experimental testing. + externalTrack.set_average_loudness(pTrack->getReplayGain().getRatio()); + + // Set main cue-point. + double cuePlayPos = pTrack->getCuePoint().getPosition(); + externalTrack.set_default_main_cue(cuePlayPos / 2); + externalTrack.set_adjusted_main_cue(cuePlayPos / 2); + + // Fill in beat grid. For now, assume a constant average BPM across + // the whole track. Note that points in the track are specified as + // "play positions", which are twice the sample offset. + BeatsPointer beats = pTrack->getBeats(); + if (beats != nullptr) { + // Note that Mixxx does not (currently) store any information about + // which beat of a bar a given beat represents. As such, in order to + // make sure we have the right phrasing, assume that the main cue point + // starts at the beginning of a bar, then move backwards towards the + // beginning of the track in 4-beat decrements to find the first beat + // in the track that also aligns with the start of a bar. + double firstBeatPlayPos = beats->findNextBeat(0); + double cueBeatPlayPos = beats->findClosestBeat(cuePlayPos); + int numBeatsToCue = beats->numBeatsInRange(firstBeatPlayPos, cueBeatPlayPos); + double firstBarAlignedBeatPlayPos = beats->findNBeatsFromSample( + cueBeatPlayPos, numBeatsToCue & ~0x3); + + // We will treat the first bar-aligned beat as beat zero. Find the + // number of beats from there until the end of the track in order to + // correctly assign an index for the last beat. + double lastBeatPlayPos = beats->findPrevBeat(sampleCount * 2); + int numBeats = beats->numBeatsInRange(firstBarAlignedBeatPlayPos, lastBeatPlayPos); + std::vector<djinterop::beatgrid_marker> beatgrid{ + {0, firstBarAlignedBeatPlayPos / 2}, {numBeats, lastBeatPlayPos / 2}}; + beatgrid = el::normalize_beatgrid(std::move(beatgrid), sampleCount); + externalTrack.set_default_beatgrid(beatgrid); + externalTrack.set_adjusted_beatgrid(beatgrid); + } else { + qInfo() << "No beats data found for track" << pTrack->getId() + << "(" << pTrack->getFileInfo().fileName() << ")"; + } + + auto cues = pTrack->getCuePoints(); + for (const CuePointer& pCue : cues) { + // We are only interested in hot cues. + if (pCue->getType() != CueType::HotCue) { + continue; + } + + int hot_cue_index = pCue->getHotCue(); // Note: Mixxx uses 0-based. + if (hot_cue_index < 0 || hot_cue_index >= 8) { + // Only supports a maximum of 8 hot cues. + qInfo() << "Skipping hot cue" << hot_cue_index + << "as the Engine Prime format only supports at most 8" + << "hot cues."; + continue; + } + + QString label = pCue->getLabel(); + if (label == "") { + label = QString("Cue %1").arg(hot_cue_index + 1); + } + + djinterop::hot_cue hc{}; + hc.label = label.toStdString(); + hc.sample_offset = pCue->getPosition() / 2; // Convert "play pos". + hc.color = el::standard_pad_colors::pads[hot_cue_index]; + externalTrack.set_hot_cue_at(hot_cue_index, hc); + } + + // Note that Mixxx does not support pre-calculated stored loops, but it will + // remember the position of a single ad-hoc loop between track loads. + // However, since this single ad-hoc loop is likely to be different in use + // from a set of stored loops (and is easily overwritten), we do not export + // it to the external database here. + externalTrack.set_loops({}); + + // Write waveform. + // Note that writing a single waveform will automatically calculate an + // overview waveform too. + if (pWaveform) { + int64_t samplesPerEntry = externalTrack.required_waveform_samples_per_entry(); + int64_t externalWaveformSize = (sampleCount / samplesPerEntry) + 1; + std::vector<djinterop::waveform_entry> externalWaveform; + externalWaveform.reserve(externalWaveformSize); + for (int64_t i = 0; i < externalWaveformSize; ++i) { + auto j = pWaveform->getDataSize() * i / externalWaveformSize; + externalWaveform.push_back({{pWaveform->getLow(j), 127}, + {pWaveform->getMid(j), 127}, + {pWaveform->getHigh(j), 127}}); + } + externalTrack.set_waveform(std::move(externalWaveform)); + } else { + qInfo() << "No waveform data found for track" << pTrack->getId() + << "(" << pTrack->getFileInfo().fileName() << ")"; + } +} + +void exportTrack( + const EnginePrimeExportRequest& request, + djinterop::database& db, + QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap, + const TrackPointer pTrack, + std::unique_ptr<Waveform> pWaveform) { + // Only export supported file types. + if (!kSupportedFileTypes.contains(pTrack->getType())) { + qInfo() << "Skipping file" << pTrack->getFileInfo().fileName() + << "(id" << pTrack->getId() << ") as its file type" + << pTrack->getType() << "is not supported"; + return; + } + + // Copy the file, if required. + auto musicFileRelativePath = exportFile(request, pTrack); + + // Export meta-data. + exportMetadata(db, + mixxxToEnginePrimeTrackIdMap, + pTrack, + std::move(pWaveform), + musicFileRelativePath); +} + +void exportCrate( + djinterop::crate& extRootCrate, + QHash<TrackId, int64_t>& mixxxToEnginePrimeTrackIdMap, + const Crate& crate, + const QList<TrackId>& trackIds) { + // Create a new crate as a sub-crate of the top-level Mixxx crate. + auto extCrate = extRootCrate.create_sub_crate(crate.getName().toStdString()); + + // Loop through all track ids in this crate and add. + for (auto iter = trackIds.cbegin(); iter != trackIds.cend(); ++iter) { + auto trackId = *iter; + auto extTrackId = mixxxToEnginePrimeTrackIdMap[trackId]; + extCrate.add_track(extTrackId); + } +} + +} // namespace + +EnginePrimeExportJob::EnginePrimeExportJob( + QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + EnginePrimeExportRequest request) + : QThread(parent), + m_pTrackCollectionManager(pTrackCollectionManager), + m_request{std::move(request)} { + // Must be collocated with the TrackCollectionManager. + DEBUG_ASSERT(m_pTrackCollectionManager != nullptr); + moveToThread(m_pTrackCollectionManager->thread()); +} + +void EnginePrimeExportJob::loadIds(QSet<CrateId> crateIds) { + // Note: this slot exists to ensure the track collection is never accessed + // from outside its own thread. + DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread()); + QMutexLocker lock{&m_mainThreadLoadMutex}; + + if (crateIds.isEmpty()) { + // No explicit crate ids specified, meaning we want to export the + // whole library, plus all non-empty crates. Start by building a list + // of unique track refs from all directories in the library. + qDebug() << "Loading all track refs and crate ids..."; + QSet<TrackRef> trackRefs; + auto dirs = m_pTrackCollectionManager->internalCollection() + ->getDirectoryDAO() + .getDirs(); + for (auto& dir : dirs) { + trackRefs.unite(m_pTrackCollectionManager->internalCollection() + ->getTrackDAO() + .getAllTrackRefs(dir) + .toSet()); + } + + m_trackRefs = trackRefs.toList(); + + // Convert a list of track refs to a list of track ids, and use that + // to identify all crates that contain those tracks. + QList<TrackId> trackIds; + for (auto& trackRef : trackRefs) { + trackIds.append(trackRef.getId()); + } + m_crateIds = m_pTrackCollectionManager->internalCollection() + ->crates() + .collectCrateIdsOfTracks(trackIds) + .toList(); + } else { + // Explicit crates have been specified to export. + qDebug() << "Loading track refs from" << crateIds.size() << "crate(s)"; + m_crateIds = crateIds.toList(); + + // Identify track refs from the specified crates. + m_trackRefs.clear(); + for (auto& crateId : crateIds) { + auto result = m_pTrackCollectionManager->internalCollection() + ->crates() + .selectCrateTracksSorted(crateId); + while (result.next()) { + auto trackId = result.trackId(); + auto location = m_pTrackCollectionManager->internalCollection() + ->getTrackDAO() + .getTrackLocation(trackId); + auto trackFile = TrackFile(location); + m_trackRefs.append(TrackRef::fromFileInfo(trackFile, trackId)); + } + } + } + + // Inform the worker thread that some main-thread loading has completed. + m_waitForMainThreadLoad.wakeAll(); +} + +void EnginePrimeExportJob::loadTrack(TrackRef trackRef) { + // Note: this slot exists to ensure the track collection is never accessed + // from outside its own thread. + DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread()); + QMutexLocker lock{&m_mainThreadLoadMutex}; + + // Load the track. + m_pLastLoadedTrack = m_pTrackCollectionManager->getOrAddTrack(trackRef); + + // Load high-resolution waveform from analysis info. + auto& analysisDao = m_pTrackCollectionManager->internalCollection()->getAnalysisDAO(); + auto waveformAnalyses = analysisDao.getAnalysesForTrackByType( + m_pLastLoadedTrack->getId(), AnalysisDao::TYPE_WAVEFORM); + if (!waveformAnalyses.isEmpty()) { + auto& waveformAnalysis = waveformAnalyses.first(); + m_pLastLoadedWaveform.reset( + WaveformFactory::loadWaveformFromAnalysis(waveformAnalysis)); + } + + // Inform the worker thread that some main-thread loading has completed. + m_waitForMainThreadLoad.wakeAll(); +} + +void EnginePrimeExportJob::loadCrate(CrateId crateId) { + // Note: this slot exists to ensure the track collection is never accessed + // from outside its own thread. + DEBUG_ASSERT(thread() == m_pTrackCollectionManager->thread()); + QMutexLocker lock{&m_mainThreadLoadMutex}; + + // Load crate details. + m_pTrackCollectionManager->internalCollection()->crates().readCrateById( + crateId, &m_lastLoadedCrate); + + // Loop through all track ids in this crate and add to a list. + auto result = m_pTrackCollectionManager->internalCollection() + ->crates() + .selectCrateTracksSorted(crateId); + m_lastLoadedCrateTrackIds.clear(); + while (result.next()) { + m_lastLoadedCrateTrackIds.append(result.trackId()); + } + + // Inform the worker thread that some main-thread loading has completed. + m_waitForMainThreadLoad.wakeAll(); +} + +void EnginePrimeExportJob::run() { + // Crate music directory if it doesn't already exist. + QDir().mkpath(m_request.musicFilesDir.path()); + + // Load ids of tracks and crates to export. + // Note that loading must happen on the same thread as the track collection + // manager, which is not the same as this method's worker thread. + { + QMutexLocker lock{&m_mainThreadLoadMutex}; + QMetaObject::invokeMethod( + this, + "loadIds", + Q_ARG(QSet<CrateId>, m_request.crateIdsToExport)); + + // We expect the `loadIds()` method to fire the below wait condition. + m_waitForMainThreadLoad.wait(&m_mainThreadLoadMutex); + } + + // Measure progress as one 'count' for each track, each crate, plus some + // additional counts for various other operations. + double maxProgress = m_trackRefs.size() + m_crateIds.size() + 2; + double currProgress = 0; + emit jobMaximum(maxProgress); + emit jobProgress(currProgress); + + // Ensure that the database exists, creati |