From 629a4b56fc399d397fc0ac118c3089bf37cab67f Mon Sep 17 00:00:00 2001 From: Adam Szmigin Date: Thu, 7 May 2020 21:50:58 +0100 Subject: Engine library export - initial implementation --- CMakeLists.txt | 16 +- src/library/export/dlglibraryexport.cpp | 209 ++++++++++ src/library/export/dlglibraryexport.h | 64 +++ src/library/export/engineprimeexportjob.cpp | 539 ++++++++++++++++++++++++++ src/library/export/engineprimeexportjob.h | 68 ++++ src/library/export/engineprimeexportrequest.h | 18 + src/library/export/exportrequest.h | 22 ++ src/library/export/libraryexporter.cpp | 60 +++ src/library/export/libraryexporter.h | 49 +++ src/library/library.cpp | 24 ++ src/library/library.h | 17 + src/library/mixxxlibraryfeature.cpp | 33 ++ src/library/mixxxlibraryfeature.h | 39 +- src/library/trackset/crate/cratefeature.cpp | 38 +- src/library/trackset/crate/cratefeature.h | 14 + src/mixxx.cpp | 33 ++ src/mixxx.h | 11 + src/widget/wmainmenubar.cpp | 11 + src/widget/wmainmenubar.h | 3 + 19 files changed, 1255 insertions(+), 13 deletions(-) create mode 100644 src/library/export/dlglibraryexport.cpp create mode 100644 src/library/export/dlglibraryexport.h create mode 100644 src/library/export/engineprimeexportjob.cpp create mode 100644 src/library/export/engineprimeexportjob.h create mode 100644 src/library/export/engineprimeexportrequest.h create mode 100644 src/library/export/exportrequest.h create mode 100644 src/library/export/libraryexporter.cpp create mode 100644 src/library/export/libraryexporter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fae18018b..f60ab84633 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1619,14 +1619,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() @@ -1671,11 +1669,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 +#include +#include +#include +#include +#include +#include +#include + +#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(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(); + m_pCratesList->setSelectionMode(QListWidget::ExtendedSelection); + populateCrates(*m_pCratesList, *m_pTrackCollectionManager->internalCollection()); + + // Read-only text fields showing key directories for export. + m_pBaseDirectoryTextField = make_parented(); + m_pBaseDirectoryTextField->setReadOnly(true); + m_pDatabaseDirectoryTextField = make_parented(); + m_pDatabaseDirectoryTextField->setReadOnly(true); + m_pMusicDirectoryTextField = make_parented(); + 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(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(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(tr("Browse")); + connect(pExportDirBrowseButton, + &QPushButton::clicked, + this, + &DlgLibraryExport::browseExportDirectory); + auto pExportDirLayout = make_parented(); + pExportDirLayout->addWidget(m_pBaseDirectoryTextField); + pExportDirLayout->addWidget(pExportDirBrowseButton); + + auto pFormLayout = make_parented(); + 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(tr("Export")); + pExportButton->setDefault(true); + connect(pExportButton, &QPushButton::clicked, this, &DlgLibraryExport::exportRequested); + auto pCancelButton = make_parented(tr("Cancel")); + connect(pCancelButton, &QPushButton::clicked, this, &QDialog::reject); + + // Arrange action buttons at bottom of dialog. + auto pButtonBarLayout = make_parented(); + pButtonBarLayout->addStretch(1); + pButtonBarLayout->addWidget(pExportButton); + pButtonBarLayout->addWidget(pCancelButton); + + auto pLayout = make_parented(); + 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) { + 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()}; + 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 +#include +#include +#include +#include +#include +#include + +#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); + + 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 m_pWholeLibraryRadio; + parented_ptr m_pCratesRadio; + parented_ptr m_pCratesList; + parented_ptr m_pBaseDirectoryTextField; + parented_ptr m_pDatabaseDirectoryTextField; + parented_ptr 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 toDjinteropKey( + track::io::key::ChromaticKey key) { + static const std::array, 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& mixxxToEnginePrimeTrackIdMap, + TrackPointer pTrack, + std::unique_ptr 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(pTrack->getDuration() * pTrack->getSampleRate()); + externalTrack.set_sampling({static_cast(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 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 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& mixxxToEnginePrimeTrackIdMap, + const TrackPointer pTrack, + std::unique_ptr 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& mixxxToEnginePrimeTrackIdMap, + const Crate& crate, + const QList& 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 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 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 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, 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, creating an empty one if not. + bool created; + djinterop::database db = el::create_or_load_database( + m_request.engineLibraryDbDir.path().toStdString(), + el::version_latest, + created); + ++currProgress; + emit jobProgress(currProgress); + + // We will build up a map from Mixxx track id to EL track id during export. + QHash mixxxToEnginePrimeTrackIdMap; + + for (auto& trackRef : m_trackRefs) { + // Load each track. + // 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, + "loadTrack", + Q_ARG(TrackRef, trackRef)); + + // We expect the `loadTrack()` method to fire the below wait condition. + m_waitForMainThreadLoad.wait(&m_mainThreadLoadMutex); + } + + if (m_cancellationRequested.loadAcquire() != 0) { + qInfo() << "Cancelling export"; + return; + } + + DEBUG_ASSERT(m_pLastLoadedTrack != nullptr); + + qInfo() << "Exporting track" << m_pLastLoadedTrack->getId().value() + << "at" << m_pLastLoadedTrack->getFileInfo().location() << "..."; + exportTrack(m_request, + db, + mixxxToEnginePrimeTrackIdMap, + m_pLastLoadedTrack, + std::move(m_pLastLoadedWaveform)); + m_pLastLoadedTrack.reset(); + + ++currProgress; + emit jobProgress(currProgress); + } + + // We will ensure that there is a special top-level crate representing the + // root of all Mixxx-exported items. Mixxx tracks and crates will exist + // underneath this crate. + auto optionalExtRootCrate = db.root_crate_by_name(kMixxxRootCrateName); + auto extRootCrate = optionalExtRootCrate + ? optionalExtRootCrate.value() + : db.create_root_crate(kMixxxRootCrateName); + for (const TrackRef& trackRef : m_trackRefs) { + // Add each track to the root crate, even if it also belongs to others. + if (!mixxxToEnginePrimeTrackIdMap.contains(trackRef.getId())) { + qInfo() << "Not adding track" << trackRef.getId() + << "to any crates, as it was not exported"; + continue; + } + + auto extTrackId = mixxxToEnginePrimeTrackIdMap.value( + trackRef.getId()); + extRootCrate.add_track(extTrackId); + } + + ++currProgress; + emit jobProgress(currProgress); + + // Export all Mixxx crates + for (const CrateId& crateId : m_crateIds) { + // Load the current crate. + // 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, + "loadCrate", + Q_ARG(CrateId, crateId)); + + // We expect the `loadCrate()` method to fire the below wait condition. + m_waitForMainThreadLoad.wait(&m_mainThreadLoadMutex); + } + + if (m_cancellationRequested.loadAcquire() != 0) { + qInfo() << "Cancelling export"; + return; + } + + qInfo() << "Exporting crate" << m_lastLoadedCrate.getId().value() << "..."; + exportCrate( + extRootCrate, + mixxxToEnginePrimeTrackIdMap, + m_lastLoadedCrate, + m_lastLoadedCrateTrackIds); + + ++currProgress; + emit jobProgress(currProgress); + } + + qInfo() << "Engine Prime Export Job completed successfully"; +} + +void EnginePrimeExportJob::cancel() { + m_cancellationRequested = 1; +} + +} // namespace mixxx diff --git a/src/library/export/engineprimeexportjob.h b/src/library/export/engineprimeexportjob.h new file mode 100644 index 0000000000..18c80908ae --- /dev/null +++ b/src/library/export/engineprimeexportjob.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "library/export/engineprimeexportrequest.h" +#include "library/trackcollectionmanager.h" +#include "library/trackset/crate/crate.h" +#include "library/trackset/crate/crateid.h" +#include "track/track.h" +#include "track/trackid.h" +#include "track/trackref.h" + +namespace mixxx { + +/// The Engine Prime export job performs the work of exporting the Mixxx +/// library to an external Engine Prime (also known as "Engine Library") +/// database, using the libdjinterop library, in accordance with the export +/// request with which it is constructed. +class EnginePrimeExportJob : public QThread { + Q_OBJECT + public: + EnginePrimeExportJob( + QObject* parent, + TrackCollectionManager* pTrackCollectionManager, + EnginePrimeExportRequest request); + + void run() override; + + signals: + void jobMaximum(int maximum); + void jobProgress(int progress); + + public slots: + void cancel(); + + private slots: + // These slots are used to load data from the Mixxx database on the main + // thread of the application, which will be different to the worker thread + // used by an instance of this class. + void loadIds(QSet crateIdsToExport); + void loadTrack(TrackRef trackRef); + void loadCrate(CrateId crateId); + + private: + QMutex m_mainThreadLoadMutex; + QWaitCondition m_waitForMainThreadLoad; + + QList m_trackRefs; + QList m_crateIds; + TrackPointer m_pLastLoadedTrack; + std::unique_ptr m_pLastLoadedWaveform; + Crate m_lastLoadedCrate; + QList m_lastLoadedCrateTrackIds; + + QAtomicInteger m_cancellationRequested; + + TrackCollectionManager* m_pTrackCollectionManager; + EnginePrimeExportRequest m_request; +}; + +} // namespace mixxx diff --git a/src/library/export/engineprimeexportrequest.h b/src/library/export/engineprimeexportrequest.h new file mode 100644 index 0000000000..d4000e65f7 --- /dev/null +++ b/src/library/export/engineprimeexportrequest.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "library/export/exportrequest.h" + +namespace mixxx { + +/// A request to export the Mixxx library to an external Engine Prime database. +struct EnginePrimeExportRequest : public ExportRequest { + /// Directory path, ending in "Engine Library", where database files will be written. + QDir engineLibraryDbDir; + + /// Directory in which to write the exported music files. + QDir musicFilesDir; +}; + +} // namespace mixxx diff --git a/src/library/export/exportrequest.h b/src/library/export/exportrequest.h new file mode 100644 index 0000000000..a5fdd4e791 --- /dev/null +++ b/src/library/export/exportrequest.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "library/trackset/crate/crateid.h" + +namespace mixxx { + +/// The ExportRequest struct is the base class for all types of request to export +/// the Mixxx library to some external location. An export to a specific kind of +/// external location is likely to need additional information to be specified in +/// the request, which can be achieved by sub-classing this struct. +struct ExportRequest { + /// Indicates whether to export only selected crates (if set to true), or + /// whether to export the whole Mixxx library (if set to false). + bool exportSelectedCrates; + + /// Set of crates to export, if `exportSelectedCrates` is set to true. + QSet crateIdsToExport; +}; + +} // namespace mixxx diff --git a/src/library/export/libraryexporter.cpp b/src/library/export/libraryexporter.cpp new file mode 100644 index 0000000000..d01d46ecc1 --- /dev/null +++ b/src/library/export/libraryexporter.cpp @@ -0,0 +1,60 @@ +#include "library/export/libraryexporter.h" + +#include +#include + +#include "library/export/engineprimeexportjob.h" +#include "util/parented_ptr.h" + +namespace mixxx { + +LibraryExporter::LibraryExporter(QWidget* parent, + UserSettingsPointer pConfig, + TrackCollectionManager* pTrackCollectionManager) + : QWidget{parent}, + m_pConfig{std::move(pConfig)}, + m_pTrackCollectionManager{pTrackCollectionManager} { +} + +void LibraryExporter::requestExportWithOptionalInitialCrate( + std::optional initialSelectedCrate) { + if (!m_pDialog) { + m_pDialog = make_parented( + this, m_pConfig, m_pTrackCollectionManager); + connect(m_pDialog.get(), + SIGNAL(startEnginePrimeExport(EnginePrimeExportRequest)), + this, + SLOT(beginEnginePrimeExport(EnginePrimeExportRequest))); + } else { + m_pDialog->show(); + m_pDialog->raise(); + m_pDialog->setWindowState( + (m_pDialog->windowState() & ~Qt::WindowMinimized) | + Qt::WindowActive); + } + + m_pDialog->setSelectedCrate(initialSelectedCrate); +} + +void LibraryExporter::beginEnginePrimeExport( + EnginePrimeExportRequest request) { + // Note that the job will run in a background thread. + auto pJobThread = make_parented( + this, + m_pTrackCollectionManager, + std::move(request)); + connect(pJobThread, &EnginePrimeExportJob::finished, pJobThread, &QObject::deleteLater); + + // Construct a dialog to monitor job progress and offer cancellation. + auto pd = make_parented(this); + pd->setLabelText(tr("Exporting to Engine Prime...")); + pd->setMinimumDuration(0); + connect(pJobThread, &EnginePrimeExportJob::jobMaximum, pd, &QProgressDialog::setMaximum); + connect(pJobThread, &EnginePrimeExportJob::jobProgress, pd, &QProgressDialog::setValue); + connect(pJobThread, &EnginePrimeExportJob::finished, pd, &QObject::deleteLater); + connect(pd, &QProgressDialog::canceled, pJobThread, &EnginePrimeExportJob::cancel); + + pJobThread->start(); +} + +} // namespace mixxx diff --git a/src/library/export/libraryexporter.h b/src/library/export/libraryexporter.h new file mode 100644 index 0000000000..73d5a9fe9d --- /dev/null +++ b/src/library/export/libraryexporter.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include "library/export/dlglibraryexport.h" +#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 LibraryExporter class allows an export of the Mixxx library to be +/// initiated. It can present a dialog that gathers information from the user +/// about the nature of the export, and schedules a job to perform the export. +class LibraryExporter : public QWidget { + Q_OBJECT + public: + LibraryExporter(QWidget* parent, + UserSettingsPointer pConfig, + TrackCollectionManager* pTrackCollectionManager); + + public slots: + /// Begin the process of a library export. + void requestExport() { + requestExportWithOptionalInitialCrate(std::nullopt); + } + void requestExportWithInitialCrate(CrateId initialSelectedCrate) { + requestExportWithOptionalInitialCrate( + std::make_optional(initialSelectedCrate)); + } + + private slots: + void beginEnginePrimeExport(EnginePrimeExportRequest request); + + private: + void requestExportWithOptionalInitialCrate( + std::optional initialSelectedCrate); + + UserSettingsPointer m_pConfig; + TrackCollectionManager* m_pTrackCollectionManager; + parented_ptr m_pDialog; +}; + +} // namespace mixxx diff --git a/src/library/library.cpp b/src/library/library.cpp index bf77ad0f2d..b71dc9c169 100644 --- a/src/library/library.cpp +++ b/src/library/library.cpp @@ -12,6 +12,9 @@ #include "library/autodj/autodjfeature.h" #include "library/banshee/bansheefeature.h" #include "library/browse/browsefeature.h" +#ifdef __ENGINEPRIME__ +#include "library/export/libraryexporter.h" +#endif #include "library/externaltrackcollection.h" #include "library/itunes/itunesfeature.h" #include "library/library_preferences.h" @@ -91,12 +94,23 @@ Library::Library( this, m_pConfig); addFeature(m_pMixxxLibraryFeature); +#ifdef __ENGINEPRIME__ + connect(m_pMixxxLibraryFeature, + &MixxxLibraryFeature::exportLibrary, + this, + &Library::exportLibrary); +#endif addFeature(new AutoDJFeature(this, m_pConfig, pPlayerManager)); m_pPlaylistFeature = new PlaylistFeature(this, UserSettingsPointer(m_pConfig)); addFeature(m_pPlaylistFeature); + m_pCrateFeature = new CrateFeature(this, m_pConfig); addFeature(m_pCrateFeature); +#ifdef __ENGINEPRIME__ + connect(m_pCrateFeature, &CrateFeature::exportAllCrates, this, &Library::exportLibrary); + connect(m_pCrateFeature, &CrateFeature::exportCrate, this, &Library::exportCrate); +#endif BrowseFeature* browseFeature = new BrowseFeature( this, m_pConfig, pRecordingManager); @@ -569,3 +583,13 @@ void Library::searchTracksInCollection(const QString& query) { emit switchToView(m_sTrackViewName); m_pSidebarModel->activateDefaultSelection(); } + +#ifdef __ENGINEPRIME__ +std::unique_ptr Library::makeLibraryExporter( + QWidget* parent) { + // New object is expected to be owned (and lifecycle-managed) + // by the supplied parent widget. + return std::make_unique( + parent, m_pConfig, m_pTrackCollectionManager); +} +#endif diff --git a/src/library/library.h b/src/library/library.h index 80dee27def..bab0abe404 100644 --- a/src/library/library.h +++ b/src/library/library.h @@ -7,6 +7,9 @@ #include #include "analyzer/analyzerprogress.h" +#ifdef __ENGINEPRIME__ +#include "library/trackset/crate/crateid.h" +#endif #include "preferences/usersettings.h" #include "track/track_decl.h" #include "track/trackid.h" @@ -33,6 +36,12 @@ class WSearchLineEdit; class WLibrarySidebar; class WLibrary; +#ifdef __ENGINEPRIME__ +namespace mixxx { +class LibraryExporter; +} // namespace mixxx +#endif + // A Library class is a container for all the model-side aspects of the library. // A library widget can be attached to the Library object by calling bindLibraryWidget. class Library: public QObject { @@ -94,6 +103,10 @@ class Library: public QObject { /// and shows the results by switching the view. void searchTracksInCollection(const QString& query); +#ifdef __ENGINEPRIME__ + std::unique_ptr makeLibraryExporter(QWidget* parent); +#endif + public slots: void slotShowTrackModel(QAbstractItemModel* model); void slotSwitchToView(const QString& view); @@ -119,6 +132,10 @@ class Library: public QObject { // emit this signal to enable/disable the cover art widget void enableCoverArtDisplay(bool); void trackSelected(TrackPointer pTrack); +#ifdef __ENGINEPRIME__ + void exportLibrary(); + void exportCrate(CrateId crateId); +#endif void setTrackTableFont(const QFont& font); void setTrackTableRowHeight(int rowHeight); diff --git a/src/library/mixxxlibraryfeature.cpp b/src/library/mixxxlibraryfeature.cpp index e7d898bf7f..ac23f06048 100644 --- a/src/library/mixxxlibraryfeature.cpp +++ b/src/library/mixxxlibraryfeature.cpp @@ -1,6 +1,9 @@ #include "library/mixxxlibraryfeature.h" #include +#ifdef __ENGINEPRIME__ +#include +#endif #include "library/basetrackcache.h" #include "library/dao/trackschema.h" @@ -19,6 +22,9 @@ #include "sources/soundsourceproxy.h" #include "util/dnd.h" #include "widget/wlibrary.h" +#ifdef __ENGINEPRIME__ +#include "widget/wlibrarysidebar.h" +#endif namespace { @@ -102,6 +108,14 @@ MixxxLibraryFeature::MixxxLibraryFeature(Library* pLibrary, pRootItem->appendChild(kHiddenTitle); m_childModel.setRootItem(std::move(pRootItem)); + +#ifdef __ENGINEPRIME__ + m_pExportLibraryAction = make_parented(tr("Export to External Library"), this); + connect(m_pExportLibraryAction.get(), + &QAction::triggered, + this, + &MixxxLibraryFeature::slotExportLibrary); +#endif } void MixxxLibraryFeature::bindLibraryWidget(WLibrary* pLibraryWidget, @@ -155,6 +169,13 @@ void MixxxLibraryFeature::searchAndActivate(const QString& query) { activate(); } +#ifdef __ENGINEPRIME__ +void MixxxLibraryFeature::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) { + // store the sidebar widget pointer for later use in onRightClick + m_pSidebarWidget = pSidebarWidget; +} +#endif + void MixxxLibraryFeature::activate() { emit showTrackModel(m_pLibraryTableModel); emit enableCoverArtDisplay(true); @@ -185,3 +206,15 @@ bool MixxxLibraryFeature::dragMoveAccept(const QUrl& url) { return SoundSourceProxy::isUrlSupported(url) || Parser::isPlaylistFilenameSupported(url.toLocalFile()); } + +#ifdef __ENGINEPRIME__ +void MixxxLibraryFeature::onRightClick(const QPoint& globalPos) { + QMenu menu(m_pSidebarWidget); + menu.addAction(m_pExportLibraryAction.get()); + menu.exec(globalPos); +} + +void MixxxLibraryFeature::slotExportLibrary() { + emit exportLibrary(); +} +#endif diff --git a/src/library/mixxxlibraryfeature.h b/src/library/mixxxlibraryfeature.h index 3dc0700fde..84d6392d08 100644 --- a/src/library/mixxxlibraryfeature.h +++ b/src/library/mixxxlibraryfeature.h @@ -1,19 +1,24 @@ #pragma once -#include -#include -#include +#include #include -#include #include -#include -#include +#include #include +#include +#include +#include +#include +#include +#include -#include "library/libraryfeature.h" #include "library/dao/trackdao.h" +#include "library/libraryfeature.h" #include "library/treeitemmodel.h" #include "preferences/usersettings.h" +#ifdef __ENGINEPRIME__ +#include "util/parented_ptr.h" +#endif class DlgHidden; class DlgMissing; @@ -35,6 +40,9 @@ class MixxxLibraryFeature final : public LibraryFeature { TreeItemModel* getChildModel() override; void bindLibraryWidget(WLibrary* pLibrary, KeyboardEventFilter* pKeyboard) override; +#ifdef __ENGINEPRIME__ + void bindSidebarWidget(WLibrarySidebar* pSidebarWidget) override; +#endif bool hasTrackTable() override { return true; @@ -45,8 +53,19 @@ class MixxxLibraryFeature final : public LibraryFeature { public slots: void activate() override; void activateChild(const QModelIndex& index) override; +#ifdef __ENGINEPRIME__ + void onRightClick(const QPoint& globalPos) override; +#endif void refreshLibraryModels(); +#ifdef __ENGINEPRIME__ + signals: + void exportLibrary(); + + private slots: + void slotExportLibrary(); +#endif + private: const QString kMissingTitle; const QString kHiddenTitle; @@ -60,4 +79,10 @@ class MixxxLibraryFeature final : public LibraryFeature { DlgMissing* m_pMissingView; DlgHidden* m_pHiddenView; + +#ifdef __ENGINEPRIME__ + parented_ptr m_pExportLibraryAction; + + QPointer m_pSidebarWidget; +#endif }; diff --git a/src/library/trackset/crate/cratefeature.cpp b/src/library/trackset/crate/cratefeature.cpp index 2cd5f088b7..0a218433ce 100644 --- a/src/library/trackset/crate/cratefeature.cpp +++ b/src/library/trackset/crate/cratefeature.cpp @@ -107,7 +107,7 @@ void CrateFeature::initActions() { &QAction::triggered, this, &CrateFeature::slotCreateImportCrate); - m_pExportPlaylistAction = make_parented(tr("Export Crate"), this); + m_pExportPlaylistAction = make_parented(tr("Export Crate as Playlist"), this); connect(m_pExportPlaylistAction.get(), &QAction::triggered, this, @@ -117,6 +117,18 @@ void CrateFeature::initActions() { &QAction::triggered, this, &CrateFeature::slotExportTrackFiles); +#ifdef __ENGINEPRIME__ + m_pExportAllCratesAction = make_parented(tr("Export to External Library"), this); + connect(m_pExportAllCratesAction.get(), + &QAction::triggered, + this, + &CrateFeature::slotExportAllCrates); + m_pExportCrateAction = make_parented(tr("Export to External Library"), this); + connect(m_pExportCrateAction.get(), + &QAction::triggered, + this, + &CrateFeature::slotExportCrate); +#endif } void CrateFeature::connectLibrary(Library* pLibrary) { @@ -315,6 +327,10 @@ void CrateFeature::onRightClick(const QPoint& globalPos) { menu.addAction(m_pCreateCrateAction.get()); menu.addSeparator(); menu.addAction(m_pCreateImportPlaylistAction.get()); +#ifdef __ENGINEPRIME__ + menu.addSeparator(); + menu.addAction(m_pExportAllCratesAction.get()); +#endif menu.exec(globalPos); } @@ -356,6 +372,9 @@ void CrateFeature::onRightClickChild( } menu.addAction(m_pExportPlaylistAction.get()); menu.addAction(m_pExportTrackFilesAction.get()); +#ifdef __ENGINEPRIME__ + menu.addAction(m_pExportCrateAction.get()); +#endif menu.exec(globalPos); } @@ -816,3 +835,20 @@ void CrateFeature::slotTrackSelected(TrackPointer pTrack) { void CrateFeature::slotResetSelectedTrack() { slotTrackSelected(TrackPointer()); } + +#ifdef __ENGINEPRIME__ +void CrateFeature::slotExportAllCrates() { + emit exportAllCrates(); +} + +void CrateFeature::slotExportCrate() { + if (!m_lastRightClickedIndex.isValid()) { + return; + } + + CrateId crateId = crateIdFromIndex(m_lastRightClickedIndex); + if (crateId.isValid()) { + emit exportCrate(crateId); + } +} +#endif diff --git a/src/library/trackset/crate/cratefeature.h b/src/library/trackset/crate/cratefeature.h index 32deb02efc..84d8e0537a 100644 --- a/src/library/trackset/crate/cratefeature.h +++ b/src/library/trackset/crate/cratefeature.h @@ -49,6 +49,12 @@ class CrateFeature : public BaseTrackSetFeature { void onRightClickChild(const QPoint& globalPos, const QModelIndex& index) override; void slotCreateCrate(); +#ifdef __ENGINEPRIME__ + signals: + void exportAllCrates(); + void exportCrate(CrateId crateId); +#endif + private slots: void slotDeleteCrate(); void slotRenameCrate(); @@ -61,6 +67,10 @@ class CrateFeature : public BaseTrackSetFeature { void slotExportPlaylist(); // Copy all of the tracks in a crate to a new directory (like a thumbdrive). void slotExportTrackFiles(); +#ifdef __ENGINEPRIME__ + void slotExportAllCrates(); + void slotExportCrate(); +#endif void slotAnalyzeCrate(); void slotCrateTableChanged(CrateId crateId); void slotCrateContentChanged(CrateId crateId); @@ -112,6 +122,10 @@ class CrateFeature : public BaseTrackSetFeature { parented_ptr m_pCreateImportPlaylistAction; parented_ptr m_pExportPlaylistAction; parented_ptr m_pExportTrackFilesAction; +#ifdef __ENGINEPRIME__ + parented_ptr m_pExportAllCratesAction; + parented_ptr m_pExportCrateAction; +#endif parented_ptr m_pAnalyzeCrateAction; QPointer m_pSidebarWidget; diff --git a/src/mixxx.cpp b/src/mixxx.cpp index d0d7462743..5d9a1f8928 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -34,6 +34,9 @@ #include "library/coverartcache.h" #include "library/library.h" #include "library/library_preferences.h" +#ifdef __ENGINEPRIME__ +#include "library/export/libraryexporter.h" +#endif #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "mixer/playerinfo.h" @@ -92,6 +95,9 @@ MixxxMainWindow::MixxxMainWindow( m_pLaunchImage(nullptr), m_pGuiTick(nullptr), m_pDeveloperToolsDlg(nullptr), +#ifdef __ENGINEPRIME__ + m_pLibraryExporter(nullptr), +#endif m_toolTipsCfg(mixxx::TooltipsPreference::TOOLTIPS_ON), m_pTouchShift(nullptr) { DEBUG_ASSERT(pApp); @@ -191,6 +197,19 @@ MixxxMainWindow::MixxxMainWindow( m_pPrefDlg->setWindowIcon(QIcon(":/images/mixxx_icon.svg")); m_pPrefDlg->setHidden(true); +#ifdef __ENGINEPRIME__ + // Initialise library exporter + m_pLibraryExporter = m_pCoreServices->getLibrary()->makeLibraryExporter(this); + connect(m_pCoreServices->getLibrary().get(), + &Library::exportLibrary, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::requestExport); + connect(m_pCoreServices->getLibrary().get(), + &Library::exportCrate, + m_pLibraryExporter.get(), + &mixxx::LibraryExporter::requestExportWithInitialCrate); +#endif + connectMenuBar(); QWidget* oldWidget = m_pCentralWidget; @@ -374,6 +393,11 @@ MixxxMainWindow::~MixxxMainWindow() { qWarning() << "WMainMenuBar was not deleted by our sendPostedEvents trick."; } +#ifdef __ENGINEPRIME__ + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting LibraryExporter"; + m_pLibraryExporter = nullptr; // is a unique_ptr +#endif + qDebug() << t.elapsed(false).debugMillisWithUnit() << "deleting DlgPreferences"; delete m_pPrefDlg; @@ -717,6 +741,15 @@ void MixxxMainWindow::connectMenuBar() { m_pCoreServices->getLibrary().get(), &Library::slotCreatePlaylist); } + +#ifdef __ENGINEPRIME__ + if (m_pLibraryExporter) { + connect(m_pMenuBar, + SIGNAL(exportLibrary()), + m_pLibraryExporter.get(), + SLOT(requestExport())); + } +#endif } void MixxxMainWindow::slotFileLoadSongPlayer(int deck) { diff --git a/src/mixxx.h b/src/mixxx.h index 2032375d6a..f784c67fbb 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -38,6 +38,12 @@ class VinylControlManager; class VisualsManager; class WMainMenuBar; +#ifdef __ENGINEPRIME__ +namespace mixxx { +class LibraryExporter; +} // namespace mixxx +#endif + /// This Class is the base class for Mixxx. /// It sets up the main window providing a menubar. /// For the main view, an instance of class MixxxView is @@ -127,6 +133,11 @@ class MixxxMainWindow : public QMainWindow { DlgPreferences* m_pPrefDlg; +#ifdef __ENGINEPRIME__ + // Library exporter + std::unique_ptr m_pLibraryExporter; +#endif + mixxx::TooltipsPreference m_toolTipsCfg; ControlPushButton* m_pTouchShift; diff --git a/src/widget/wmainmenubar.cpp b/src/widget/wmainmenubar.cpp index 62907573dc..10c4e10ab3 100644 --- a/src/widget/wmainmenubar.cpp +++ b/src/widget/wmainmenubar.cpp @@ -124,6 +124,17 @@ void WMainMenuBar::initialize() { connect(this, &WMainMenuBar::internalLibraryScanActive, pLibraryRescan, &QAction::setDisabled); pLibraryMenu->addAction(pLibraryRescan); +#ifdef __ENGINEPRIME__ + QString exportTitle = tr("E&xport Library for Engine Prime"); + QString exportText = tr("Export the library for another format"); + auto pLibraryExport = new QAction(exportTitle, this); + pLibraryExport->setStatusTip(exportText); + pLibraryExport->setWhatsThis(buildWhatsThis(exportTitle, exportText)); + pLibraryExport->setCheckable(false); + connect(pLibraryExport, SIGNAL(triggered()), this, SIGNAL(exportLibrary())); + pLibraryMenu->addAction(pLibraryExport); +#endif + pLibraryMenu->addSeparator(); QString createPlaylistTitle = tr("Create &New Playlist"); diff --git a/src/widget/wmainmenubar.h b/src/widget/wmainmenubar.h index afcc3785f6..1514c3db65 100644 --- a/src/widget/wmainmenubar.h +++ b/src/widget/wmainmenubar.h @@ -55,6 +55,9 @@ class WMainMenuBar : public QMenuBar { void loadTrackToDeck(int deck); void reloadSkin(); void rescanLibrary(); +#ifdef __ENGINEPRIME__ + void exportLibrary(); +#endif void showAbout(); void showPreferences(); void toggleDeveloperTools(bool toggle); -- cgit v1.2.3